diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md deleted file mode 100644 index 87fa57a89..000000000 --- a/.github/ISSUE_TEMPLATE/1-issue.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Issue -about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏 ---- - -## Checklist - - - -- [ ] Raised initially as discussion #... -- [ ] This is not a feature request suitable for implementation outside this project. Please elaborate what it is: - - [ ] compatibility fix for new Django/Python version ... - - [ ] other type of bug fix - - [ ] other type of improvement that does not touch existing code or change existing behavior (e.g. wrapper for new Django field) -- [ ] I have reduced the issue to the simplest possible case. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 382fc521a..0ba2c5d9d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,3 +4,4 @@ contact_links: url: https://github.com/encode/django-rest-framework/discussions about: > The "Discussions" forum is where you want to start. 💖 + Please note that at this point in its lifespan, we consider Django REST framework to be feature-complete. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d08655451..45e745ccc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,30 +3,30 @@ name: CI on: push: branches: - - master + - main pull_request: jobs: tests: name: Python ${{ matrix.python-version }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: python-version: - - '3.8' - - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' + - '3.14' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true cache: 'pip' cache-dependency-path: 'requirements/*.txt' @@ -34,29 +34,30 @@ jobs: run: python -m pip install --upgrade pip setuptools virtualenv wheel - name: Install dependencies - run: python -m pip install --upgrade codecov tox + run: python -m pip install --upgrade tox - name: Run tox targets for ${{ matrix.python-version }} run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-') - name: Run extra tox targets - if: ${{ matrix.python-version == '3.9' }} + if: ${{ matrix.python-version == '3.13' }} run: | tox -e base,dist,docs - name: Upload coverage - run: | - codecov -e TOXENV,DJANGO + uses: codecov/codecov-action@v5 + with: + env_vars: TOXENV,DJANGO test-docs: name: Test documentation links - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version: '3.13' - name: Install dependencies run: pip install -r requirements/requirements-documentation.txt diff --git a/.github/workflows/mkdocs-deploy.yml b/.github/workflows/mkdocs-deploy.yml new file mode 100644 index 000000000..7b7c5df2a --- /dev/null +++ b/.github/workflows/mkdocs-deploy.yml @@ -0,0 +1,29 @@ +name: mkdocs + +on: + push: + branches: + - main + paths: + - docs/** + - docs_theme/** + - requirements/requirements-documentation.txt + - mkdocs.yml + - .github/workflows/mkdocs-deploy.yml + +jobs: + deploy: + runs-on: ubuntu-latest + environment: github-pages + permissions: + contents: write + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + steps: + - uses: actions/checkout@v5 + - run: git fetch --no-tags --prune --depth=1 origin gh-pages + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: pip install -r requirements/requirements-documentation.txt + - run: mkdocs gh-deploy diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 892235175..8432fe48b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -3,7 +3,7 @@ name: pre-commit on: push: branches: - - master + - main pull_request: jobs: @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.10" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8939dd3db..a5ea46db1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,3 +31,17 @@ repos: hooks: - id: codespell exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js + additional_dependencies: + # python doesn't come with a toml parser prior to 3.11 + - "tomli; python_version < '3.11'" + +- repo: https://github.com/asottile/pyupgrade + rev: v3.19.1 + hooks: + - id: pyupgrade + args: ["--py39-plus", "--keep-percent-format"] + +- repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.6.0 + hooks: + - id: pyproject-fmt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb01f8bf7..af7d55f13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,6 @@ At this point in its lifespan we consider Django REST framework to be essentially feature-complete. We may accept pull requests that track the continued development of Django versions, but would prefer not to accept new features or code formatting changes. -Apart from minor documentation changes, the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion. +Apart from minor documentation changes, the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only open a pull request if you've been recommended to do so **after discussion**. The [Contributing guide in the documentation](https://www.django-rest-framework.org/community/contributing/) gives some more information on our process and code of conduct. diff --git a/README.md b/README.md index 6e62fb39a..b2bada7b4 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Some reasons you might want to use REST framework: # Requirements -* Python 3.8+ -* Django 4.2, 5.0, 5.1 +* Python 3.10+ +* Django 4.2, 5.0, 5.1, 5.2 We **highly recommend** and only officially support the latest patch release of each Python and Django series. @@ -179,8 +179,8 @@ Please see the [security policy][security-policy]. [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 +[coverage-status-image]: https://img.shields.io/codecov/c/github/encode/django-rest-framework/main.svg +[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=main [pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg [pypi]: https://pypi.org/project/djangorestframework/ [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework @@ -188,16 +188,16 @@ Please see the [security policy][security-policy]. [funding]: https://fund.django-rest-framework.org/topics/funding/ [sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors -[sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png -[stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png -[spacinov-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/spacinov-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 -[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png -[svix-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/svix-premium.png -[zuplo-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/zuplo-readme.png +[sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/sentry-readme.png +[stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/stream-readme.png +[spacinov-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/spacinov-readme.png +[retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/retool-readme.png +[bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/bitio-readme.png +[posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/posthog-readme.png +[cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/cryptapi-readme.png +[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/fezto-readme.png +[svix-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/svix-premium.png +[zuplo-img]: https://raw.githubusercontent.com/encode/django-rest-framework/main/docs/img/premium/zuplo-readme.png [sentry-url]: https://getsentry.com/welcome/ [stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage diff --git a/SECURITY.md b/SECURITY.md index a92a1b0cf..88ff092a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,8 +2,6 @@ ## Reporting a Vulnerability -Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team). +**Please report security issues by emailing security@encode.io**. - **Please report security issues by emailing security@djangoproject.com**. - - The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. +The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 8409a83c8..84e58bf4b 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -454,6 +454,12 @@ There are currently two forks of this project. More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html). +## django-pyoidc + +[dango-pyoidc][django_pyoidc] adds support for OpenID Connect (OIDC) authentication. This allows you to delegate user management to an Identity Provider, which can be used to implement Single-Sign-On (SSO). It provides support for most uses-cases, such as customizing how token info are mapped to user models, using OIDC audiences for access control, etc. + +More information can be found in the [Documentation](https://django-pyoidc.readthedocs.io/latest/index.html). + [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -490,4 +496,5 @@ More information can be found in the [Documentation](https://django-rest-durin.r [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless [django-rest-authemail]: https://github.com/celiao/django-rest-authemail [django-rest-durin]: https://github.com/eshaan7/django-rest-durin -[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware \ No newline at end of file +[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware +[django-pyoidc] : https://github.com/makinacorpus/django_pyoidc diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index c4ab215c8..4beea30d0 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -85,7 +85,7 @@ def get_user_list(request): **NOTE:** The [`cache_page`][page] decorator only caches the `GET` and `HEAD` responses with status 200. -[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 +[page]: https://docs.djangoproject.com/en/stable/topics/cache/#the-per-view-cache +[cookie]: https://docs.djangoproject.com/en/stable/topics/http/decorators/#django.views.decorators.vary.vary_on_cookie +[headers]: https://docs.djangoproject.com/en/stable/topics/http/decorators/#django.views.decorators.vary.vary_on_headers +[decorator]: https://docs.djangoproject.com/en/stable/topics/class-based-views/intro/#decorating-the-class diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 33590046b..4a31828a0 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -269,5 +269,5 @@ The [drf-standardized-errors][drf-standardized-errors] package provides an excep [cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/ [authentication]: authentication.md -[django-custom-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views +[django-custom-error-views]: https://docs.djangoproject.com/en/stable/topics/http/views/#customizing-error-views [drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 5cbedd964..8278e2a2f 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -42,7 +42,7 @@ Set to false if this field is not required to be present during deserialization. Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation. -Defaults to `True`. If you're using [Model Serializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) default value will be `False` if you have specified `blank=True` or `default` or `null=True` at your field in your `Model`. +Defaults to `True`. If you're using [Model Serializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer), the default value will be `False` when you have specified a `default`, or when the corresponding `Model` field has `blank=True` or `null=True` and is not part of a unique constraint at the same time. (Note that without a `default` value, [unique constraints will cause the field to be required](https://www.django-rest-framework.org/api-guide/validators/#optional-fields).) ### `default` @@ -377,13 +377,16 @@ A Duration representation. Corresponds to `django.db.models.fields.DurationField` The `validated_data` for these fields will contain a `datetime.timedelta` instance. -The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`. -**Signature:** `DurationField(max_value=None, min_value=None)` +**Signature:** `DurationField(format=api_settings.DURATION_FORMAT, max_value=None, min_value=None)` +* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DURATION_FORMAT` settings key, which will be `'django'` unless set. Formats are described below. Setting this value to `None` indicates that Python `timedelta` objects should be returned by `to_representation`. In this case the date encoding will be determined by the renderer. * `max_value` Validate that the duration provided is no greater than this value. * `min_value` Validate that the duration provided is no less than this value. +#### `DurationField` formats +Format may either be the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style intervals should be used (eg `'P4DT1H15M20S'`), or `'django'` which indicates that Django interval format `'[DD] [HH:[MM:]]ss[.uuuuuu]'` should be used (eg: `'4 1:15:20'`). + --- # Choice selection fields @@ -552,7 +555,7 @@ For further examples on `HiddenField` see the [validators](validators.md) docume --- -**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). +**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). --- @@ -857,4 +860,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 +[django-docs-select-related]: https://docs.djangoproject.com/en/stable/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 ff5f3c775..d36d4ce95 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -235,7 +235,7 @@ For example: search_fields = ['=username', '=email'] -By default, the search parameter is named `'search'`, but this may be overridden with the `SEARCH_PARAM` setting. +By default, the search parameter is named `'search'`, but this may be overridden with the `SEARCH_PARAM` setting in the `REST_FRAMEWORK` configuration. To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: @@ -257,7 +257,7 @@ The `OrderingFilter` class supports simple query parameter controlled ordering o ![Ordering Filter](../img/ordering-filter.png) -By default, the query parameter is named `'ordering'`, but this may be overridden with the `ORDERING_PARAM` setting. +By default, the query parameter is named `'ordering'`, but this may be overridden with the `ORDERING_PARAM` setting in the `REST_FRAMEWORK` configuration. For example, to order users by username: @@ -367,6 +367,6 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter [drf-url-filter]: https://github.com/manjitkumar/drf-url-filters -[HStoreField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#hstorefield -[JSONField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#jsonfield +[HStoreField]: https://docs.djangoproject.com/en/stable/ref/contrib/postgres/fields/#hstorefield +[JSONField]: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.JSONField [postgres-search]: https://docs.djangoproject.com/en/stable/ref/contrib/postgres/search/ diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 410e3518d..70bfa7992 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -374,8 +374,6 @@ Allowing `PUT` as create operations is problematic, as it necessarily exposes in Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but from version 3.0 onwards we now use 404 behavior as the default, due to it being simpler and more obvious. -If you need to generic PUT-as-create behavior you may want to include something like [this `AllowPUTAsCreateMixin` class](https://gist.github.com/tomchristie/a2ace4577eff2c603b1b) as a mixin to your views. - --- # Third party packages @@ -395,4 +393,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 +[django-docs-select-related]: https://docs.djangoproject.com/en/stable/ref/models/querysets/#django.db.models.query.QuerySet.select_related diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 775888fb6..c6d9f9338 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -201,7 +201,7 @@ As with `DjangoModelPermissions` you can use custom model permissions by overrid --- -**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests and are using django-guardian for your object-level permissions backend, you'll want to consider using the `DjangoObjectPermissionsFilter` class provided by the [`djangorestframework-guardian2` package][django-rest-framework-guardian2]. It ensures that list endpoints only return results including objects for which the user has appropriate view permissions. +**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests and are using django-guardian for your object-level permissions backend, you'll want to consider using the `DjangoObjectPermissionsFilter` class provided by the [`djangorestframework-guardian` package][django-rest-framework-guardian]. It ensures that list endpoints only return results including objects for which the user has appropriate view permissions. --- @@ -356,6 +356,6 @@ The [Django Rest Framework PSQ][drf-psq] package is an extension that gives supp [rest-framework-roles]: https://github.com/Pithikos/rest-framework-roles [djangorestframework-api-key]: https://florimondmanca.github.io/djangorestframework-api-key/ [django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters -[django-rest-framework-guardian2]: https://github.com/johnthagen/django-rest-framework-guardian2 +[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian [drf-access-policy]: https://github.com/rsinger86/drf-access-policy [drf-psq]: https://github.com/drf-psq/drf-psq diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 56eb61e43..7c4eece4b 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -628,12 +628,16 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys. +The [rest-framework-gm2m-relations][drf-gm2m-relations] library provides read/write serialization for [django-gm2m][django-gm2m-field]. + [cite]: http://users.ece.utexas.edu/~adnan/pike.html [reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward [routers]: https://www.django-rest-framework.org/api-guide/routers#defaultrouter [generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations +[drf-gm2m-relations]: https://github.com/mojtabaakbari221b/rest-framework-gm2m-relations +[django-gm2m-field]: https://github.com/tkhyn/django-gm2m [django-intermediary-manytomany]: https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany [dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects [to_internal_value]: https://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index d48f785ab..7a6bd39f4 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -525,7 +525,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## LaTeX -[Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. +[Rest Framework Latex] provides a renderer that outputs PDFs using Lualatex. It is maintained by [Pebble (S/F Software)][mypebble]. [cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index d6bdeb235..d2be3991f 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -350,6 +350,6 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions [drf-extensions-nested-viewsets]: https://chibisov.github.io/drf-extensions/docs/#nested-routes [drf-extensions-collection-level-controllers]: https://chibisov.github.io/drf-extensions/docs/#collection-level-controllers [drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name -[url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces -[include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include -[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters +[url-namespace-docs]: https://docs.djangoproject.com/en/stable/topics/http/urls/#url-namespaces +[include-api-reference]: https://docs.djangoproject.com/en/stable/ref/urls/#include +[path-converters-topic-reference]: https://docs.djangoproject.com/en/stable/topics/http/urls/#path-converters diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index c387af972..c74d00cb7 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -392,7 +392,7 @@ introspection. #### `get_operation_id()` -There must be a unique [operationid](openapi-operationid) for each operation. +There must be a unique [operationid][openapi-operationid] for each operation. By default the `operationId` is deduced from the model name, serializer name or view name. The operationId looks like "listItems", "retrieveItem", "updateItem", etc. The `operationId` is camelCase by convention. @@ -451,14 +451,14 @@ 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 +[cite]: https://www.heroku.com/blog/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 +[openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#specification-extensions +[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#operationObject [openapi-tags]: https://swagger.io/specification/#tagObject -[openapi-operationid]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-17 -[openapi-components]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#componentsObject -[openapi-reference]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#referenceObject +[openapi-operationid]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#fixed-fields-17 +[openapi-components]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#componentsObject +[openapi-reference]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#referenceObject [openapi-generator]: https://github.com/OpenAPITools/openapi-generator [swagger-codegen]: https://github.com/swagger-api/swagger-codegen [info-object]: https://swagger.io/specification/#infoObject diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index eae79b62f..3ce8f887f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -233,7 +233,7 @@ Serializer classes can also include reusable validators that are applied to the class EventSerializer(serializers.Serializer): name = serializers.CharField() - room_number = serializers.IntegerField(choices=[101, 102, 103, 201]) + room_number = serializers.ChoiceField(choices=[101, 102, 103, 201]) date = serializers.DateField() class Meta: @@ -1189,6 +1189,10 @@ The [drf-writable-nested][drf-writable-nested] package provides writable nested The [drf-encrypt-content][drf-encrypt-content] package helps you encrypt your data, serialized through ModelSerializer. It also contains some helper functions. Which helps you to encrypt your data. +## Shapeless Serializers + +The [drf-shapeless-serializers][drf-shapeless-serializers] package provides dynamic serializer configuration capabilities, allowing runtime field selection, renaming, attribute modification, and nested relationship configuration without creating multiple serializer classes. It helps eliminate serializer boilerplate while providing flexible API responses. + [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md @@ -1212,3 +1216,4 @@ The [drf-encrypt-content][drf-encrypt-content] package helps you encrypt your da [djangorestframework-queryfields]: https://djangorestframework-queryfields.readthedocs.io/ [drf-writable-nested]: https://github.com/beda-software/drf-writable-nested [drf-encrypt-content]: https://github.com/oguzhancelikarslan/drf-encrypt-content +[drf-shapeless-serializers]: https://github.com/khaledsukkar2/drf-shapeless-serializers diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 47e2ce993..2a070b77e 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -314,6 +314,15 @@ May be a list including the string `'iso-8601'` or Python [strftime format][strf Default: `['iso-8601']` + +#### DURATION_FORMAT + +Indicates the default format that should be used for rendering the output of `DurationField` serializer fields. If `None`, then `DurationField` serializer fields will return Python `timedelta` objects, and the duration encoding will be determined by the renderer. + +May be any of `None`, `'iso-8601'` or `'django'` (the format accepted by `django.utils.dateparse.parse_duration`). + +Default: `'django'` + --- ## Encodings @@ -460,4 +469,4 @@ Default: `None` [cite]: https://www.python.org/dev/peps/pep-0020/ [rfc4627]: https://www.ietf.org/rfc/rfc4627.txt [heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses -[strftime]: https://docs.python.org/3/library/time.html#time.strftime +[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 261df80f2..97b432ddd 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -25,9 +25,12 @@ The `APIRequestFactory` class supports an almost identical API to Django's stand factory = APIRequestFactory() request = factory.post('/notes/', {'title': 'new idea'}) + # Using the standard RequestFactory API to encode JSON data + request = factory.post('/notes/', {'title': 'new idea'}, content_type='application/json') + #### Using the `format` argument -Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: +Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a wide set of request formats. When using this argument, the factory will select an appropriate renderer and its configured `content_type`. For example: # Create a JSON POST request factory = APIRequestFactory() @@ -41,7 +44,7 @@ To support a wider set of request formats, or change the default format, [see th If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example: - request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') + request = factory.post('/notes/', yaml.dump({'title': 'new idea'}), content_type='application/yaml') #### PUT and PATCH with form data @@ -102,6 +105,20 @@ This means that setting attributes directly on the request object may not always request.user = user response = view(request) +If you want to test a request involving the REST framework’s 'Request' object, you’ll need to manually transform it first: + + class DummyView(APIView): + ... + + factory = APIRequestFactory() + request = factory.get('/', {'demo': 'test'}) + drf_request = DummyView().initialize_request(request) + assert drf_request.query_params == {'demo': ['test']} + + request = factory.post('/', {'example': 'test'}) + drf_request = DummyView().initialize_request(request) + assert drf_request.data.get('example') == 'test' + --- ## Forcing CSRF validation @@ -414,5 +431,5 @@ For example, to add support for using `format='html'` in test requests, you migh [requestfactory]: https://docs.djangoproject.com/en/stable/topics/testing/advanced/#django.test.client.RequestFactory [configuration]: #configuration [refresh_from_db_docs]: https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.refresh_from_db -[session_objects]: https://requests.readthedocs.io/en/master/user/advanced/#session-objects +[session_objects]: https://requests.readthedocs.io/en/latest/user/advanced/#session-objects [provided_test_case_classes]: https://docs.djangoproject.com/en/stable/topics/testing/tools/#provided-test-case-classes diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 4c58fa713..0ea8b4158 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -45,7 +45,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C } } -The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. +The rates used in `DEFAULT_THROTTLE_RATES` can be specified over a period of second, minute, hour or day. The period must be specified after the `/` separator using `s`, `m`, `h` or `d`, respectively. For increased clarity, extended units such as `second`, `minute`, `hour`, `day` or even abbreviations like `sec`, `min`, `hr` are allowed, as only the first character is relevant to identify the rate. You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class-based views. diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e181d4c61..e3407e8a3 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -13,7 +13,7 @@ Most of the time you're dealing with validation in REST framework you'll simply However, sometimes you'll want to place your validation logic into reusable components, so that it can easily be reused throughout your codebase. This can be achieved by using validator functions and validator classes. -## Validation in REST framework +## Validation in REST framework Validation in Django REST framework serializers is handled a little differently to how validation works in Django's `ModelForm` class. @@ -48,7 +48,7 @@ If we open up the Django shell using `manage.py shell` we can now CustomerReportSerializer(): id = IntegerField(label='ID', read_only=True) time_raised = DateTimeField(read_only=True) - reference = CharField(max_length=20, validators=[]) + reference = CharField(max_length=20, validators=[UniqueValidator(queryset=CustomerReportRecord.objects.all())]) description = CharField(style={'type': 'textarea'}) The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field. @@ -75,7 +75,7 @@ This validator should be applied to *serializer fields*, like so: validators=[UniqueValidator(queryset=BlogPost.objects.all())] ) -## UniqueTogetherValidator +## UniqueTogetherValidator This validator can be used to enforce `unique_together` constraints on model instances. It has two required arguments, and a single optional `messages` argument: @@ -92,7 +92,7 @@ The validator should be applied to *serializer classes*, like so: # ... class Meta: # ToDo items belong to a parent list, and have an ordering defined - # by the 'position' field. No two items in a given list may share + # by the 'position' field. No two items in a given list may share # the same position. validators = [ UniqueTogetherValidator( @@ -166,7 +166,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden --- -**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). +**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). --- diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index b293de75a..05a35c3d6 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -186,8 +186,13 @@ The available decorators are: * `@authentication_classes(...)` * `@throttle_classes(...)` * `@permission_classes(...)` +* `@content_negotiation_class(...)` +* `@metadata_class(...)` +* `@versioning_class(...)` -Each of these decorators takes a single argument which must be a list or tuple of classes. +Each of these decorators is equivalent to setting their respective [api policy attributes][api-policy-attributes]. + +All decorators take a single argument. The ones that end with `_class` expect a single class while the ones ending in `_classes` expect a list or tuple of classes. ## View schema decorator @@ -224,4 +229,5 @@ You may pass `None` in order to exclude the view from schema generation. [throttling]: throttling.md [schemas]: schemas.md [classy-drf]: http://www.cdrf.co +[api-policy-attributes]: views.md#api-policy-attributes diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 43007e95d..22acfe327 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -128,6 +128,8 @@ You may inspect these attributes to adjust behavior based on the current action. permission_classes = [IsAdminUser] return [permission() for permission in permission_classes] +**Note**: the `action` attribute is not available in the `get_parsers`, `get_authenticators` and `get_content_negotiator` methods, as it is set _after_ they are called in the framework lifecycle. If you override one of these methods and try to access the `action` attribute in them, you will get an `AttributeError` error. + ## Marking extra actions for routing If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns. diff --git a/docs/community/3.0-announcement.md b/docs/community/3.0-announcement.md index 0cb79fc2e..cec61f337 100644 --- a/docs/community/3.0-announcement.md +++ b/docs/community/3.0-announcement.md @@ -961,5 +961,5 @@ You can follow development on the GitHub site, where we use [milestones to indic [kickstarter]: https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3 [sponsors]: https://www.django-rest-framework.org/community/kickstarter-announcement/#sponsors -[mixins.py]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py +[mixins.py]: https://github.com/encode/django-rest-framework/blob/main/rest_framework/mixins.py [django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files diff --git a/docs/community/3.1-announcement.md b/docs/community/3.1-announcement.md index 641f313d0..2b4b83d57 100644 --- a/docs/community/3.1-announcement.md +++ b/docs/community/3.1-announcement.md @@ -46,7 +46,7 @@ The cursor based pagination renders a more simple style of control: The pagination API was previously only able to alter the pagination style in the body of the response. The API now supports being able to write pagination information in response headers, making it possible to use pagination schemes that use the `Link` or `Content-Range` headers. -For more information, see the [custom pagination styles](../api-guide/pagination/#custom-pagination-styles) documentation. +For more information, see the [custom pagination styles](../api-guide/pagination.md#custom-pagination-styles) documentation. --- diff --git a/docs/community/3.16-announcement.md b/docs/community/3.16-announcement.md new file mode 100644 index 000000000..b8f460ae7 --- /dev/null +++ b/docs/community/3.16-announcement.md @@ -0,0 +1,42 @@ + + +# Django REST framework 3.16 + +At the Internet, on March 28th, 2025, we are happy to announce the release of Django REST framework 3.16. + +## Updated Django and Python support + +The latest release now fully supports Django 5.1 and the upcoming 5.2 LTS as well as Python 3.13. + +The current minimum versions of Django is now 4.2 and Python 3.9. + +## Django LoginRequiredMiddleware + +The new `LoginRequiredMiddleware` introduced by Django 5.1 can now be used alongside Django REST Framework, however it is not honored for API views as an equivalent behaviour can be configured via `DEFAULT_AUTHENTICATION_CLASSES`. See [our dedicated section](../api-guide/authentication.md#django-51-loginrequiredmiddleware) in the docs for more information. + +## Improved support for UniqueConstraint + +The generation of validators for [UniqueConstraint](https://docs.djangoproject.com/en/stable/ref/models/constraints/#uniqueconstraint) has been improved to support better nullable fields and constraints with conditions. + +## Other fixes and improvements + +There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour. + +See the [release notes](release-notes.md) page for a complete listing. diff --git a/docs/community/3.3-announcement.md b/docs/community/3.3-announcement.md index 24f493dcd..3f6427c53 100644 --- a/docs/community/3.3-announcement.md +++ b/docs/community/3.3-announcement.md @@ -54,7 +54,7 @@ The `ModelSerializer` and `HyperlinkedModelSerializer` classes should now includ [forms-api]: ../topics/html-and-forms.md [ajax-form]: https://github.com/encode/ajax-form -[jsonfield]: ../api-guide/fields#jsonfield +[jsonfield]: ../api-guide/fields.md#jsonfield [accept-headers]: ../topics/browser-enhancements.md#url-based-accept-headers [method-override]: ../topics/browser-enhancements.md#http-header-based-method-overriding [django-supported-versions]: https://www.djangoproject.com/download/#supported-versions diff --git a/docs/community/3.4-announcement.md b/docs/community/3.4-announcement.md index 2954b36b8..03ef6fc41 100644 --- a/docs/community/3.4-announcement.md +++ b/docs/community/3.4-announcement.md @@ -179,16 +179,16 @@ The full set of itemized release notes [are available here][release-notes]. [moss]: mozilla-grant.md [funding]: funding.md [core-api]: https://www.coreapi.org/ -[command-line-client]: api-clients#command-line-client -[client-library]: api-clients#python-client-library +[command-line-client]: https://github.com/encode/django-rest-framework/blob/3.4.7/docs/topics/api-clients.md#command-line-client +[client-library]: https://github.com/encode/django-rest-framework/blob/3.4.7/docs/topics/api-clients.md#python-client-library [core-json]: https://www.coreapi.org/specification/encoding/#core-json-encoding [swagger]: https://openapis.org/specification [hyperschema]: https://json-schema.org/latest/json-schema-hypermedia.html [api-blueprint]: https://apiblueprint.org/ -[tut-7]: ../tutorial/7-schemas-and-client-libraries/ -[schema-generation]: ../api-guide/schemas/ +[tut-7]: https://github.com/encode/django-rest-framework/blob/3.4.7/docs/tutorial/7-schemas-and-client-libraries.md +[schema-generation]: ../api-guide/schemas.md [api-clients]: https://github.com/encode/django-rest-framework/blob/3.14.0/docs/topics/api-clients.md [milestone]: https://github.com/encode/django-rest-framework/milestone/35 -[release-notes]: release-notes#34 -[metadata]: ../api-guide/metadata/#custom-metadata-classes +[release-notes]: ./release-notes.md#34x-series +[metadata]: ../api-guide/metadata.md#custom-metadata-classes [gh3751]: https://github.com/encode/django-rest-framework/issues/3751 diff --git a/docs/community/3.5-announcement.md b/docs/community/3.5-announcement.md index 43a628dd4..de558fead 100644 --- a/docs/community/3.5-announcement.md +++ b/docs/community/3.5-announcement.md @@ -254,9 +254,9 @@ in version 3.3 and raised a deprecation warning in 3.4. Its usage is now mandato [funding]: funding.md [uploads]: https://core-api.github.io/python-client/api-guide/utils/#file [downloads]: https://core-api.github.io/python-client/api-guide/codecs/#downloadcodec -[schema-generation-api]: ../api-guide/schemas/#schemagenerator -[schema-docs]: ../api-guide/schemas/#schemas-as-documentation -[schema-view]: ../api-guide/schemas/#the-get_schema_view-shortcut +[schema-generation-api]: ../api-guide/schemas.md#schemagenerator +[schema-docs]: ../api-guide/schemas.md#schemas-as-documentation +[schema-view]: ../api-guide/schemas.md#get_schema_view [django-rest-raml]: https://github.com/encode/django-rest-raml [raml-image]: ../img/raml.png [raml-codec]: https://github.com/core-api/python-raml-codec diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 5dea6426d..797bf72e3 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -30,9 +30,7 @@ The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines f # Issues -Our contribution process is that the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion. - -Some tips on good potential issue reporting: +Our contribution process is that the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Some tips on good potential issue reporting: * Django REST framework is considered feature-complete. Please do not file requests to change behavior, unless it is required for security reasons or to maintain compatibility with upcoming Django or Python versions. * Search the GitHub project page for related items, and make sure you're running the latest version of REST framework before reporting an issue. @@ -83,12 +81,45 @@ To run the tests, clone the repository, and then: # Run the tests ./runtests.py +--- + +**Note:** if your tests require access to the database, do not forget to inherit from `django.test.TestCase` or use the `@pytest.mark.django_db()` decorator. + +For example, with TestCase: + + from django.test import TestCase + + class MyDatabaseTest(TestCase): + def test_something(self): + # Your test code here + pass + +Or with decorator: + + import pytest + + @pytest.mark.django_db() + class MyDatabaseTest: + def test_something(self): + # Your test code here + pass + +You can reuse existing models defined in `tests/models.py` for your tests. + +--- + ### Test options Run using a more concise output style. ./runtests.py -q + +If you do not want the output to be captured (for example, to see print statements directly), you can use the `-s` flag. + + ./runtests.py -s + + Run the tests for a given test case. ./runtests.py MyTestCase @@ -101,6 +132,7 @@ 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 @@ -206,13 +238,12 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [so-filter]: https://stackexchange.com/filters/66475/rest-framework -[issues]: https://github.com/encode/django-rest-framework/issues?state=open [pep-8]: https://www.python.org/dev/peps/pep-0008/ [build-status]: ../img/build-status.png [pull-requests]: https://help.github.com/articles/using-pull-requests [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 +[docs]: https://github.com/encode/django-rest-framework/tree/main/docs [mou]: http://mouapp.com/ [repo]: https://github.com/encode/django-rest-framework [how-to-fork]: https://help.github.com/articles/fork-a-repo/ diff --git a/docs/community/funding.md b/docs/community/funding.md index 10e09bf71..7bca4bae4 100644 --- a/docs/community/funding.md +++ b/docs/community/funding.md @@ -114,7 +114,7 @@ If you use REST framework commercially we strongly encourage you to invest in it Signing up for a paid plan will: * Directly contribute to faster releases, more features, and higher quality software. -* Allow more time to be invested in documentation, issue triage, and community support. +* Allow more time to be invested in keeping the package up to date. * Safeguard the future development of REST framework. REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development. @@ -134,18 +134,6 @@ REST framework continues to be open-source and permissively licensed, but we fir --- -## What future funding will enable - -* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries. -* Better authentication defaults, possibly bringing JWT & CORS support into the core package. -* Securing the community & operations manager position long-term. -* Opening up and securing a part-time position to focus on ticket triage and resolution. -* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation. - -Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project. - ---- - ## What our sponsors and users say > As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem. @@ -165,6 +153,8 @@ DRF is one of the core reasons why Django is top choice among web frameworks tod > > — Andrew Conti, Django REST framework user +Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project. + --- ## Individual plan diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 3512ac84f..35cec5276 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -31,7 +31,7 @@ Team members have the following responsibilities. Further notes for maintainers: -* Code changes should come in the form of a pull request - do not push directly to master. +* Code changes should come in the form of a pull request - do not push directly to main. * Maintainers should typically not merge their own pull requests. * Each issue/pull request should have exactly one label once triaged. * Search for un-triaged issues with [is:open no:label][un-triaged]. @@ -53,12 +53,13 @@ The following template should be used for the description of the issue, and serv Checklist: - - [ ] Create pull request for [release notes](https://github.com/encode/django-rest-framework/blob/master/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/encode/django-rest-framework/milestones/***). + - [ ] Create pull request for [release notes](https://github.com/encode/django-rest-framework/blob/mains/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/encode/django-rest-framework/milestones/***). - [ ] Update supported versions: - - [ ] `setup.py` `python_requires` list - - [ ] `setup.py` Python & Django version trove classifiers + - [ ] `pyproject.toml` `python_requires` list + - [ ] `pyproject.toml` Python & Django version trove classifiers - [ ] `README` Python & Django versions - [ ] `docs` Python & Django versions + - [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/encode/django-rest-framework/blob/master/rest_framework/__init__.py). - [ ] Ensure documentation validates - Build and serve docs `mkdocs serve` @@ -66,7 +67,9 @@ The following template should be used for the description of the issue, and serv - [ ] Confirm with @tomchristie that release is finalized and ready to go. - [ ] Ensure that release date is included in pull request. - [ ] Merge the release pull request. - - [ ] Push the package to PyPI with `./setup.py publish`. + - [ ] Install the release tools: `pip install build twine` + - [ ] Build the package: `python -m build` + - [ ] Push the package to PyPI with `twine upload dist/*` - [ ] Tag the release, with `git tag -a *.*.* -m 'version *.*.*'; git push --tags`. - [ ] Deploy the documentation with `mkdocs gh-deploy`. - [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework). diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 3e5d3ebc5..ae59ae000 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -36,6 +36,153 @@ You can determine your currently installed version using `pip show`: --- +## 3.16.x series + +### 3.16.1 + +**Date**: 6th August 2025 + +This release fixes a few bugs, clean-up some old code paths for unsupported Python versions and improve translations. + +#### Minor changes + +* Cleanup optional `backports.zoneinfo` dependency and conditions on unsupported Python 3.8 and lower in [#9681](https://github.com/encode/django-rest-framework/pull/9681). Python versions prior to 3.9 were already unsupported so this shouldn't be a breaking change. + +#### Bug fixes + +* Fix regression in `unique_together` validation with `SerializerMethodField` in [#9712](https://github.com/encode/django-rest-framework/pull/9712) +* Fix `UniqueTogetherValidator` to handle fields with `source` attribute in [#9688](https://github.com/encode/django-rest-framework/pull/9688) +* Drop HTML line breaks on long headers in browsable API in [#9438](https://github.com/encode/django-rest-framework/pull/9438) + +#### Translations + +* Add Kazakh locale support in [#9713](https://github.com/encode/django-rest-framework/pull/9713) +* Update translations for Korean translations in [#9571](https://github.com/encode/django-rest-framework/pull/9571) +* Update German translations in [#9676](https://github.com/encode/django-rest-framework/pull/9676) +* Update Chinese translations in [#9675](https://github.com/encode/django-rest-framework/pull/9675) +* Update Arabic translations-sal in [#9595](https://github.com/encode/django-rest-framework/pull/9595) +* Update Persian translations in [#9576](https://github.com/encode/django-rest-framework/pull/9576) +* Update Spanish translations in [#9701](https://github.com/encode/django-rest-framework/pull/9701) +* Update Turkish Translations in [#9749](https://github.com/encode/django-rest-framework/pull/9749) +* Fix some typos in Brazilian Portuguese translations in [#9673](https://github.com/encode/django-rest-framework/pull/9673) + +#### Documentation + +* Removed reference to GitHub Issues and Discussions in [#9660](https://github.com/encode/django-rest-framework/pull/9660) +* Add `drf-restwind` and update outdated images in `browsable-api.md` in [#9680](https://github.com/encode/django-rest-framework/pull/9680) +* Updated funding page to represent current scope in [#9686](https://github.com/encode/django-rest-framework/pull/9686) +* Fix broken Heroku JSON Schema link in [#9693](https://github.com/encode/django-rest-framework/pull/9693) +* Update Django documentation links to use stable version in [#9698](https://github.com/encode/django-rest-framework/pull/9698) +* Expand docs on unique constraints cause 'required=True' in [#9725](https://github.com/encode/django-rest-framework/pull/9725) +* Revert extension back from `djangorestframework-guardian2` to `djangorestframework-guardian` in [#9734](https://github.com/encode/django-rest-framework/pull/9734) +* Add note to tutorial about required `request` in serializer context when using `HyperlinkedModelSerializer` in [#9732](https://github.com/encode/django-rest-framework/pull/9732) + +#### Internal changes + +* Update GitHub Actions to use Ubuntu 24.04 for testing in [#9677](https://github.com/encode/django-rest-framework/pull/9677) +* Update test matrix to use Django 5.2 stable version in [#9679](https://github.com/encode/django-rest-framework/pull/9679) +* Add `pyupgrade` to `pre-commit` hooks in [#9682](https://github.com/encode/django-rest-framework/pull/9682) +* Fix test with Django 5 when `pytz` is available in [#9715](https://github.com/encode/django-rest-framework/pull/9715) + +#### New Contributors + +* [`@araggohnxd`](https://github.com/araggohnxd) made their first contribution in [#9673](https://github.com/encode/django-rest-framework/pull/9673) +* [`@mbeijen`](https://github.com/mbeijen) made their first contribution in [#9660](https://github.com/encode/django-rest-framework/pull/9660) +* [`@stefan6419846`](https://github.com/stefan6419846) made their first contribution in [#9676](https://github.com/encode/django-rest-framework/pull/9676) +* [`@ren000thomas`](https://github.com/ren000thomas) made their first contribution in [#9675](https://github.com/encode/django-rest-framework/pull/9675) +* [`@ulgens`](https://github.com/ulgens) made their first contribution in [#9682](https://github.com/encode/django-rest-framework/pull/9682) +* [`@bukh-sal`](https://github.com/bukh-sal) made their first contribution in [#9595](https://github.com/encode/django-rest-framework/pull/9595) +* [`@rezatn0934`](https://github.com/rezatn0934) made their first contribution in [#9576](https://github.com/encode/django-rest-framework/pull/9576) +* [`@Rohit10jr`](https://github.com/Rohit10jr) made their first contribution in [#9693](https://github.com/encode/django-rest-framework/pull/9693) +* [`@kushibayev`](https://github.com/kushibayev) made their first contribution in [#9713](https://github.com/encode/django-rest-framework/pull/9713) +* [`@alihassancods`](https://github.com/alihassancods) made their first contribution in [#9732](https://github.com/encode/django-rest-framework/pull/9732) +* [`@kulikjak`](https://github.com/kulikjak) made their first contribution in [#9715](https://github.com/encode/django-rest-framework/pull/9715) +* [`@Natgho`](https://github.com/Natgho) made their first contribution in [#9749](https://github.com/encode/django-rest-framework/pull/9749) + +**Full Changelog**: https://github.com/encode/django-rest-framework/compare/3.16.0...3.16.1 + +### 3.16.0 + +**Date**: 28th March 2025 + +This release is considered a significant release to improve upstream support with Django and Python. Some of these may change the behaviour of existing features and pre-existing behaviour. Specifically, some fixes were added to around the support of `UniqueConstraint` with nullable fields which will improve built-in serializer validation. + +#### Features + +* Add official support for Django 5.1 and its new `LoginRequiredMiddleware` in [#9514](https://github.com/encode/django-rest-framework/pull/9514) and [#9657](https://github.com/encode/django-rest-framework/pull/9657) +* Add official Django 5.2a1 support in [#9634](https://github.com/encode/django-rest-framework/pull/9634) +* Add support for Python 3.13 in [#9527](https://github.com/encode/django-rest-framework/pull/9527) and [#9556](https://github.com/encode/django-rest-framework/pull/9556) +* Support Django 2.1+ test client JSON data automatically serialized in [#6511](https://github.com/encode/django-rest-framework/pull/6511) and fix a regression in [#9615](https://github.com/encode/django-rest-framework/pull/9615) + +#### Bug fixes + +* Fix unique together validator to respect condition's fields from `UniqueConstraint` in [#9360](https://github.com/encode/django-rest-framework/pull/9360) +* Fix raising on nullable fields part of `UniqueConstraint` in [#9531](https://github.com/encode/django-rest-framework/pull/9531) +* Fix `unique_together` validation with source in [#9482](https://github.com/encode/django-rest-framework/pull/9482) +* Added protections to `AttributeError` raised within properties in [#9455](https://github.com/encode/django-rest-framework/pull/9455) +* Fix `get_template_context` to handle also lists in [#9467](https://github.com/encode/django-rest-framework/pull/9467) +* Fix "Converter is already registered" deprecation warning. in [#9512](https://github.com/encode/django-rest-framework/pull/9512) +* Fix noisy warning and accept integers as min/max values of `DecimalField` in [#9515](https://github.com/encode/django-rest-framework/pull/9515) +* Fix usages of `open()` in `setup.py` in [#9661](https://github.com/encode/django-rest-framework/pull/9661) + +#### Translations + +* Add some missing Chinese translations in [#9505](https://github.com/encode/django-rest-framework/pull/9505) +* Fix spelling mistakes in Farsi language were corrected in [#9521](https://github.com/encode/django-rest-framework/pull/9521) +* Fixing and adding missing Brazilian Portuguese translations in [#9535](https://github.com/encode/django-rest-framework/pull/9535) + +#### Removals + +* Remove support for Python 3.8 in [#9670](https://github.com/encode/django-rest-framework/pull/9670) +* Remove long deprecated code from request wrapper in [#9441](https://github.com/encode/django-rest-framework/pull/9441) +* Remove deprecated `AutoSchema._get_reference` method in [#9525](https://github.com/encode/django-rest-framework/pull/9525) + +#### Documentation and internal changes + +* Provide tests for hashing of `OperandHolder` in [#9437](https://github.com/encode/django-rest-framework/pull/9437) +* Update documentation: Add `adrf` third party package in [#9198](https://github.com/encode/django-rest-framework/pull/9198) +* Update tutorials links in Community contributions docs in [#9476](https://github.com/encode/django-rest-framework/pull/9476) +* Fix usage of deprecated Django function in example from docs in [#9509](https://github.com/encode/django-rest-framework/pull/9509) +* Move path converter docs into a separate section in [#9524](https://github.com/encode/django-rest-framework/pull/9524) +* Add test covering update view without `queryset` attribute in [#9528](https://github.com/encode/django-rest-framework/pull/9528) +* Fix Transifex link in [#9541](https://github.com/encode/django-rest-framework/pull/9541) +* Fix example `httpie` call in docs in [#9543](https://github.com/encode/django-rest-framework/pull/9543) +* Fix example for serializer field with choices in docs in [#9563](https://github.com/encode/django-rest-framework/pull/9563) +* Remove extra `<>` in validators example in [#9590](https://github.com/encode/django-rest-framework/pull/9590) +* Update `strftime` link in the docs in [#9624](https://github.com/encode/django-rest-framework/pull/9624) +* Switch to codecov GHA in [#9618](https://github.com/encode/django-rest-framework/pull/9618) +* Add note regarding availability of the `action` attribute in 'Introspecting ViewSet actions' docs section in [#9633](https://github.com/encode/django-rest-framework/pull/9633) +* Improved description of allowed throttling rates in documentation in [#9640](https://github.com/encode/django-rest-framework/pull/9640) +* Add `rest-framework-gm2m-relations` package to the list of 3rd party libraries in [#9063](https://github.com/encode/django-rest-framework/pull/9063) +* Fix a number of typos in the test suite in the docs in [#9662](https://github.com/encode/django-rest-framework/pull/9662) +* Add `django-pyoidc` as a third party authentication library in [#9667](https://github.com/encode/django-rest-framework/pull/9667) + +#### New Contributors + +* [`@maerteijn`](https://github.com/maerteijn) made their first contribution in [#9198](https://github.com/encode/django-rest-framework/pull/9198) +* [`@FraCata00`](https://github.com/FraCata00) made their first contribution in [#9444](https://github.com/encode/django-rest-framework/pull/9444) +* [`@AlvaroVega`](https://github.com/AlvaroVega) made their first contribution in [#9451](https://github.com/encode/django-rest-framework/pull/9451) +* [`@james`](https://github.com/james)-mchugh made their first contribution in [#9455](https://github.com/encode/django-rest-framework/pull/9455) +* [`@ifeanyidavid`](https://github.com/ifeanyidavid) made their first contribution in [#9479](https://github.com/encode/django-rest-framework/pull/9479) +* [`@p`](https://github.com/p)-schlickmann made their first contribution in [#9480](https://github.com/encode/django-rest-framework/pull/9480) +* [`@akkuman`](https://github.com/akkuman) made their first contribution in [#9505](https://github.com/encode/django-rest-framework/pull/9505) +* [`@rafaelgramoschi`](https://github.com/rafaelgramoschi) made their first contribution in [#9509](https://github.com/encode/django-rest-framework/pull/9509) +* [`@Sinaatkd`](https://github.com/Sinaatkd) made their first contribution in [#9521](https://github.com/encode/django-rest-framework/pull/9521) +* [`@gtkacz`](https://github.com/gtkacz) made their first contribution in [#9535](https://github.com/encode/django-rest-framework/pull/9535) +* [`@sliverc`](https://github.com/sliverc) made their first contribution in [#9556](https://github.com/encode/django-rest-framework/pull/9556) +* [`@gabrielromagnoli1987`](https://github.com/gabrielromagnoli1987) made their first contribution in [#9543](https://github.com/encode/django-rest-framework/pull/9543) +* [`@cheehong1030`](https://github.com/cheehong1030) made their first contribution in [#9563](https://github.com/encode/django-rest-framework/pull/9563) +* [`@amansharma612`](https://github.com/amansharma612) made their first contribution in [#9590](https://github.com/encode/django-rest-framework/pull/9590) +* [`@Gluroda`](https://github.com/Gluroda) made their first contribution in [#9616](https://github.com/encode/django-rest-framework/pull/9616) +* [`@deepakangadi`](https://github.com/deepakangadi) made their first contribution in [#9624](https://github.com/encode/django-rest-framework/pull/9624) +* [`@EXG1O`](https://github.com/EXG1O) made their first contribution in [#9633](https://github.com/encode/django-rest-framework/pull/9633) +* [`@decadenza`](https://github.com/decadenza) made their first contribution in [#9640](https://github.com/encode/django-rest-framework/pull/9640) +* [`@mojtabaakbari221b`](https://github.com/mojtabaakbari221b) made their first contribution in [#9063](https://github.com/encode/django-rest-framework/pull/9063) +* [`@mikemanger`](https://github.com/mikemanger) made their first contribution in [#9661](https://github.com/encode/django-rest-framework/pull/9661) +* [`@gbip`](https://github.com/gbip) made their first contribution in [#9667](https://github.com/encode/django-rest-framework/pull/9667) + +**Full Changelog**: https://github.com/encode/django-rest-framework/compare/3.15.2...3.16.0 + ## 3.15.x series ### 3.15.2 @@ -121,7 +268,7 @@ Date: 15th March 2024 * Fix 404 when page query parameter is empty string [[#8578](https://github.com/encode/django-rest-framework/pull/8578)] * Fixes instance check in ListSerializer.to_representation [[#8726](https://github.com/encode/django-rest-framework/pull/8726)] [[#8727](https://github.com/encode/django-rest-framework/pull/8727)] * FloatField will crash if the input is a number that is too big [[#8725](https://github.com/encode/django-rest-framework/pull/8725)] -* Add missing DurationField to SimpleMetada label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)] +* Add missing DurationField to SimpleMetadata label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)] * Add support for Python 3.11 [[#8752](https://github.com/encode/django-rest-framework/pull/8752)] * Make request consistently available in pagination classes [[#8764](https://github.com/encode/django-rest-framework/pull/9764)] * Possibility to remove trailing zeros on DecimalFields representation [[#6514](https://github.com/encode/django-rest-framework/pull/6514)] @@ -428,7 +575,7 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10. * Allow hashing of ErrorDetail. [#5932][gh5932] * Correct schema parsing for JSONField [#5878][gh5878] * Render descriptions (from help_text) using safe [#5869][gh5869] -* Removed input value from deault_error_message [#5881][gh5881] +* Removed input value from default_error_message [#5881][gh5881] * Added min_value/max_value support in DurationField [#5643][gh5643] * Fixed instance being overwritten in pk-only optimization try/except block [#5747][gh5747] * Fixed AttributeError from items filter when value is None [#5981][gh5981] diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 593836411..a4ad2db1e 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -32,7 +32,7 @@ We suggest adding your package to the [REST Framework][rest-framework-grid] grid #### Adding to the Django REST framework docs -Create a [Pull Request][drf-create-pr] or [Issue][drf-create-issue] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section. +Create a [Pull Request][drf-create-pr] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section. #### Announce on the discussion group. @@ -44,7 +44,7 @@ Django REST Framework has a growing community of developers, packages, and resou Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages][rest-framework-grid]. -To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr]. +To submit new content, [create a pull request][drf-create-pr]. ## Async Support @@ -62,6 +62,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF. * [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers. * [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses. +* [dango-pyoidc][django-pyoidc] adds support for OpenID Connect (OIDC) authentication. ### Permissions @@ -87,6 +88,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [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. +* [drf-shapeless-serializers][drf-shapeless-serializers] - Dynamically assemble, configure, and shape your Django Rest Framework serializers at runtime, much like connecting Lego bricks. ### Serializer fields @@ -125,7 +127,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters. * [django-url-filter][django-url-filter] - Allows a safe way to filter data via human-friendly URLs. It is a generic library which is not tied to DRF but it provides easy integration with DRF. * [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. -* [django-rest-framework-guardian2][django-rest-framework-guardian2] - Provides integration with django-guardian, including the `DjangoObjectPermissionsFilter` previously found in DRF. +* [django-rest-framework-guardian][django-rest-framework-guardian] - Provides integration with django-guardian, including the `DjangoObjectPermissionsFilter` previously found in DRF. ### Misc @@ -159,6 +161,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ### Customization +* [drf-restwind][drf-restwind] - a modern re-imagining of the Django REST Framework utilizes TailwindCSS and DaisyUI to provide flexible and customizable UI solutions with minimal coding effort. * [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5. * [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design. @@ -170,13 +173,12 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [pypi-register]: https://pypi.org/account/register/ [semver]: https://semver.org/ [tox-docs]: https://tox.readthedocs.io/en/latest/ -[drf-compat]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/compat.py +[drf-compat]: https://github.com/encode/django-rest-framework/blob/main/rest_framework/compat.py [rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/ [drf-create-pr]: https://github.com/encode/django-rest-framework/compare -[drf-create-issue]: https://github.com/encode/django-rest-framework/issues/new [authentication]: ../api-guide/authentication.md [permissions]: ../api-guide/permissions.md -[third-party-packages]: ../topics/third-party-packages/#existing-third-party-packages +[third-party-packages]: #existing-third-party-packages [discussion-group]: https://groups.google.com/forum/#!forum/django-rest-framework [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit @@ -241,7 +243,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-dataclasses]: https://github.com/oxan/djangorestframework-dataclasses [django-restql]: https://github.com/yezyilomo/django-restql [djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt -[django-rest-framework-guardian2]: https://github.com/johnthagen/django-rest-framework-guardian2 +[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian [drf-viewset-profiler]: https://github.com/fvlima/drf-viewset-profiler [djangorestframework-features]: https://github.com/cloudcode-hungary/django-rest-framework-features/ [django-elasticsearch-dsl-drf]: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf @@ -254,5 +256,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-requestlogs]: https://github.com/Raekkeri/django-requestlogs [drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors [drf-api-action]: https://github.com/Ori-Roza/drf-api-action +[drf-restwind]: https://github.com/youzarsiph/drf-restwind [drf-redesign]: https://github.com/youzarsiph/drf-redesign [drf-material]: https://github.com/youzarsiph/drf-material +[django-pyoidc]: https://github.com/makinacorpus/django_pyoidc +[drf-shapeless-serializers]: https://github.com/khaledsukkar2/drf-shapeless-serializers diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index b128160da..427bdd2d7 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -12,7 +12,7 @@ There are a wide range of resources available for learning and using Django REST - + @@ -28,7 +28,6 @@ There are a wide range of resources available for learning and using Django REST * [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework] * [Django REST Framework - An Introduction][drf-an-intro] * [Django REST Framework Tutorial][drf-tutorial] -* [Django REST Framework Course][django-rest-framework-course] * [Building a RESTful API with Django REST Framework][building-a-restful-api-with-drf] * [Getting Started with Django REST Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs] * [End to End Web App with Django REST Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs] @@ -41,8 +40,8 @@ There are a wide range of resources available for learning and using Django REST * [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2] * [Creating a Production Ready API with Python and Django REST Framework – Part 3][creating-a-production-ready-api-with-python-and-drf-part3] * [Creating a Production Ready API with Python and Django REST Framework – Part 4][creating-a-production-ready-api-with-python-and-drf-part4] -* [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog] -* [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list] +* [Django Polls Tutorial API][django-polls-api] +* [Django REST Framework Tutorial: Todo API][django-rest-framework-todo-api] * [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog] @@ -51,11 +50,11 @@ There are a wide range of resources available for learning and using Django REST ### Talks * [Level Up! Rethinking the Web API Framework][pycon-us-2017] -* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit] +* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-toolkit] * [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy] * [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014] * [Django REST Framework: Schemas, Hypermedia & Client Libraries][pycon-uk-2016] - +* [Finally Understand Authentication in Django REST Framework][django-con-2018] ### Tutorials @@ -105,7 +104,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/ [cdrf.co]:http://www.cdrf.co [medium-django-rest-framework]: https://medium.com/django-rest-framework -[django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework [pycon-uk-2016]: https://www.youtube.com/watch?v=FjmiGh7OqVg [django-under-hood-2014]: https://www.youtube.com/watch?v=3cSsbe-tA0E [integrating-pandas-drf-and-bokeh]: https://web.archive.org/web/20180104205117/http://machinalis.com/blog/pandas-django-rest-framework-bokeh/ @@ -121,10 +119,10 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/ [creating-a-production-ready-api-with-python-and-drf-part3]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-3/ [creating-a-production-ready-api-with-python-and-drf-part4]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-4/ -[django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/ -[django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/ +[django-polls-api]: https://learndjango.com/tutorials/django-polls-tutorial-api +[django-rest-framework-todo-api]: https://learndjango.com/tutorials/django-rest-framework-tutorial-todo-api [django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ -[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk +[full-fledged-rest-api-with-django-oauth-toolkit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk [drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww [building-a-rest-api-using-django-and-drf]: https://www.youtube.com/watch?v=PwssEec3IRw [drf-tutorials]: https://www.youtube.com/watch?v=axRCBgbOJp8&list=PLJtp8Jm8EDzjgVg9vVyIUMoGyqtegj7FH @@ -139,3 +137,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/ [doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/ [developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/ +[django-con-2018]: https://youtu.be/pY-oje5b5Qk?si=AOU6tLi0IL1_pVzq \ No newline at end of file diff --git a/docs/img/books/dfa-40-cover.jpg b/docs/img/books/dfa-40-cover.jpg new file mode 100644 index 000000000..cc47312c7 Binary files /dev/null and b/docs/img/books/dfa-40-cover.jpg differ diff --git a/docs/img/drf-m-api-root.png b/docs/img/drf-m-api-root.png new file mode 100644 index 000000000..02a756287 Binary files /dev/null and b/docs/img/drf-m-api-root.png differ diff --git a/docs/img/drf-m-detail-view.png b/docs/img/drf-m-detail-view.png new file mode 100644 index 000000000..33e3515d8 Binary files /dev/null and b/docs/img/drf-m-detail-view.png differ diff --git a/docs/img/drf-m-list-view.png b/docs/img/drf-m-list-view.png new file mode 100644 index 000000000..a7771957d Binary files /dev/null and b/docs/img/drf-m-list-view.png differ diff --git a/docs/img/drf-r-api-root.png b/docs/img/drf-r-api-root.png new file mode 100644 index 000000000..5704cc350 Binary files /dev/null and b/docs/img/drf-r-api-root.png differ diff --git a/docs/img/drf-r-detail-view.png b/docs/img/drf-r-detail-view.png new file mode 100644 index 000000000..2959c7201 Binary files /dev/null and b/docs/img/drf-r-detail-view.png differ diff --git a/docs/img/drf-r-list-view.png b/docs/img/drf-r-list-view.png new file mode 100644 index 000000000..1930b5dc0 Binary files /dev/null and b/docs/img/drf-r-list-view.png differ diff --git a/docs/img/drf-rw-api-root.png b/docs/img/drf-rw-api-root.png new file mode 100644 index 000000000..15b498b9b Binary files /dev/null and b/docs/img/drf-rw-api-root.png differ diff --git a/docs/img/drf-rw-detail-view.png b/docs/img/drf-rw-detail-view.png new file mode 100644 index 000000000..6e821acda Binary files /dev/null and b/docs/img/drf-rw-detail-view.png differ diff --git a/docs/img/drf-rw-list-view.png b/docs/img/drf-rw-list-view.png new file mode 100644 index 000000000..625b5c7c9 Binary files /dev/null and b/docs/img/drf-rw-list-view.png differ diff --git a/docs/img/rfm.png b/docs/img/rfm.png deleted file mode 100644 index 7c82621af..000000000 Binary files a/docs/img/rfm.png and /dev/null differ diff --git a/docs/img/rfr.png b/docs/img/rfr.png deleted file mode 100644 index 1854511d0..000000000 Binary files a/docs/img/rfr.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index 24ae81672..87330f5af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Django (4.2, 5.0, 5.1) -* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) +* Django (4.2, 5.0, 5.1, 5.2) +* Python (3.10, 3.11, 3.12, 3.13, 3.14) We **highly recommend** and only officially support the latest patch release of each Python and Django series. @@ -196,9 +196,7 @@ For priority support please sign up for a [professional or premium sponsorship p ## Security -Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team). - -**Please report security issues by emailing security@djangoproject.com**. +**Please report security issues by emailing security@encode.io**. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index fe35be8b3..8cf530b7a 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -81,22 +81,43 @@ For more specific CSS tweaks than simply overriding the default bootstrap theme ### Third party packages for customization -You can use a third party package for customization, rather than doing it by yourself. Here is 2 packages for customizing the API: +You can use a third party package for customization, rather than doing it by yourself. Here is 3 packages for customizing the API: -* [rest-framework-redesign][rest-framework-redesign] - A package for customizing the API using Bootstrap 5. Modern and sleek design, it comes with the support for dark mode. -* [rest-framework-material][rest-framework-material] - Material design for Django REST Framework. +* [drf-restwind][drf-restwind] - a modern re-imagining of the Django REST Framework utilizes TailwindCSS and DaisyUI to provide flexible and customizable UI solutions with minimal coding effort. +* [drf-redesign][drf-redesign] - A package for customizing the API using Bootstrap 5. Modern and sleek design, it comes with the support for dark mode. +* [drf-material][drf-material] - Material design for Django REST Framework. --- -![Django REST Framework Redesign][rfr] +![API Root][drf-rw-api-root] -*Screenshot of the rest-framework-redesign* +![List View][drf-rw-list-view] + +![Detail View][drf-rw-detail-view] + +*Screenshots of the drf-restwind* --- -![Django REST Framework Material][rfm] +--- -*Screenshot of the rest-framework-material* +![API Root][drf-r-api-root] + +![List View][drf-r-list-view] + +![Detail View][drf-r-detail-view] + +*Screenshot of the drf-redesign* + +--- + +![API Root][drf-m-api-root] + +![List View][drf-m-api-root] + +![Detail View][drf-m-api-root] + +*Screenshot of the drf-material* --- @@ -197,7 +218,15 @@ There are [a variety of packages for autocomplete widgets][autocomplete-packages [bcomponentsnav]: https://getbootstrap.com/2.3.2/components.html#navbar [autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/ [django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light -[rest-framework-redesign]: https://github.com/youzarsiph/rest-framework-redesign -[rest-framework-material]: https://github.com/youzarsiph/rest-framework-material -[rfr]: ../img/rfr.png -[rfm]: ../img/rfm.png +[drf-restwind]: https://github.com/youzarsiph/drf-restwind +[drf-rw-api-root]: ../img/drf-rw-api-root.png +[drf-rw-list-view]: ../img/drf-rw-list-view.png +[drf-rw-detail-view]: ../img/drf-rw-detail-view.png +[drf-redesign]: https://github.com/youzarsiph/drf-redesign +[drf-r-api-root]: ../img/drf-r-api-root.png +[drf-r-list-view]: ../img/drf-r-list-view.png +[drf-r-detail-view]: ../img/drf-r-detail-view.png +[drf-material]: https://github.com/youzarsiph/drf-material +[drf-m-api-root]: ../img/drf-m-api-root.png +[drf-m-list-view]: ../img/drf-m-list-view.png +[drf-m-detail-view]: ../img/drf-m-detail-view.png diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index b9bf67acb..99393bbaa 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -16,14 +16,18 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o Before we do anything else we'll create a new virtual environment, using [venv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on. - python3 -m venv env - source env/bin/activate +```bash +python3 -m venv env +source env/bin/activate +``` Now that we're inside a virtual environment, we can install our package requirements. - pip install django - pip install djangorestframework - pip install pygments # We'll be using this for the code highlighting +```bash +pip install django +pip install djangorestframework +pip install pygments # We'll be using this for the code highlighting +``` **Note:** To exit the virtual environment at any time, just type `deactivate`. For more information see the [venv documentation][venv]. @@ -32,21 +36,27 @@ Now that we're inside a virtual environment, we can install our package requirem Okay, we're ready to get coding. To get started, let's create a new project to work with. - cd ~ - django-admin startproject tutorial - cd tutorial +```bash +cd ~ +django-admin startproject tutorial +cd tutorial +``` Once that's done we can create an app that we'll use to create a simple Web API. - python manage.py startapp snippets +```bash +python manage.py startapp snippets +``` We'll need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`. Let's edit the `tutorial/settings.py` file: - INSTALLED_APPS = [ - ... - 'rest_framework', - 'snippets', - ] +```text +INSTALLED_APPS = [ + ... + 'rest_framework', + 'snippets', +] +``` Okay, we're ready to roll. @@ -54,64 +64,72 @@ Okay, we're ready to roll. For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets/models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself. - from django.db import models - from pygments.lexers import get_all_lexers - from pygments.styles import get_all_styles +```python +from django.db import models +from pygments.lexers import get_all_lexers +from pygments.styles import get_all_styles - LEXERS = [item for item in get_all_lexers() if item[1]] - LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) - STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()]) +LEXERS = [item for item in get_all_lexers() if item[1]] +LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) +STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()]) - class Snippet(models.Model): - created = models.DateTimeField(auto_now_add=True) - title = models.CharField(max_length=100, blank=True, default='') - code = models.TextField() - linenos = models.BooleanField(default=False) - language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100) - style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100) +class Snippet(models.Model): + created = models.DateTimeField(auto_now_add=True) + title = models.CharField(max_length=100, blank=True, default="") + code = models.TextField() + linenos = models.BooleanField(default=False) + language = models.CharField( + choices=LANGUAGE_CHOICES, default="python", max_length=100 + ) + style = models.CharField(choices=STYLE_CHOICES, default="friendly", max_length=100) - class Meta: - ordering = ['created'] + class Meta: + ordering = ["created"] +``` We'll also need to create an initial migration for our snippet model, and sync the database for the first time. - python manage.py makemigrations snippets - python manage.py migrate snippets +```bash +python manage.py makemigrations snippets +python manage.py migrate snippets +``` ## Creating a Serializer class The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. - from rest_framework import serializers - from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES +```python +from rest_framework import serializers +from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES - class SnippetSerializer(serializers.Serializer): - id = serializers.IntegerField(read_only=True) - title = serializers.CharField(required=False, allow_blank=True, max_length=100) - code = serializers.CharField(style={'base_template': 'textarea.html'}) - linenos = serializers.BooleanField(required=False) - language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') - style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') +class SnippetSerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + title = serializers.CharField(required=False, allow_blank=True, max_length=100) + code = serializers.CharField(style={"base_template": "textarea.html"}) + linenos = serializers.BooleanField(required=False) + language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default="python") + style = serializers.ChoiceField(choices=STYLE_CHOICES, default="friendly") - def create(self, validated_data): - """ - Create and return a new `Snippet` instance, given the validated data. - """ - return Snippet.objects.create(**validated_data) + def create(self, validated_data): + """ + Create and return a new `Snippet` instance, given the validated data. + """ + return Snippet.objects.create(**validated_data) - def update(self, instance, validated_data): - """ - Update and return an existing `Snippet` instance, given the validated data. - """ - instance.title = validated_data.get('title', instance.title) - instance.code = validated_data.get('code', instance.code) - instance.linenos = validated_data.get('linenos', instance.linenos) - instance.language = validated_data.get('language', instance.language) - instance.style = validated_data.get('style', instance.style) - instance.save() - return instance + def update(self, instance, validated_data): + """ + Update and return an existing `Snippet` instance, given the validated data. + """ + instance.title = validated_data.get("title", instance.title) + instance.code = validated_data.get("code", instance.code) + instance.linenos = validated_data.get("linenos", instance.linenos) + instance.language = validated_data.get("language", instance.language) + instance.style = validated_data.get("style", instance.style) + instance.save() + return instance +``` The first part of the serializer class defines the fields that get serialized/deserialized. The `create()` and `update()` methods define how fully fledged instances are created or modified when calling `serializer.save()` @@ -125,57 +143,71 @@ We can actually also save ourselves some time by using the `ModelSerializer` cla Before we go any further we'll familiarize ourselves with using our new Serializer class. Let's drop into the Django shell. - python manage.py shell +```bash +python manage.py shell +``` Okay, once we've got a few imports out of the way, let's create a couple of code snippets to work with. - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer - from rest_framework.renderers import JSONRenderer - from rest_framework.parsers import JSONParser +```pycon +>>> from snippets.models import Snippet +>>> from snippets.serializers import SnippetSerializer +>>> from rest_framework.renderers import JSONRenderer +>>> from rest_framework.parsers import JSONParser - snippet = Snippet(code='foo = "bar"\n') - snippet.save() +>>> snippet = Snippet(code='foo = "bar"\n') +>>> snippet.save() - snippet = Snippet(code='print("hello, world")\n') - snippet.save() +>>> snippet = Snippet(code='print("hello, world")\n') +>>> snippet.save() +``` We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances. - serializer = SnippetSerializer(snippet) - serializer.data - # {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'} +```pycon +>>> serializer = SnippetSerializer(snippet) +>>> serializer.data +{'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'} +``` At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. - content = JSONRenderer().render(serializer.data) - content - # b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}' +```pycon +>>> content = JSONRenderer().render(serializer.data) +>>> content +b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}' +``` Deserialization is similar. First we parse a stream into Python native datatypes... - import io +```pycon +>>> import io - stream = io.BytesIO(content) - data = JSONParser().parse(stream) +>>> stream = io.BytesIO(content) +>>> data = JSONParser().parse(stream) +``` ...then we restore those native datatypes into a fully populated object instance. - serializer = SnippetSerializer(data=data) - serializer.is_valid() - # True - serializer.validated_data - # {'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'} - serializer.save() - # +```pycon +>>> serializer = SnippetSerializer(data=data) +>>> serializer.is_valid() +True +>>> serializer.validated_data +{'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'} +>>> serializer.save() + +``` Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer. We can also serialize querysets instead of model instances. To do so we simply add a `many=True` flag to the serializer arguments. - serializer = SnippetSerializer(Snippet.objects.all(), many=True) - serializer.data - # [{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}] +```pycon +>>> serializer = SnippetSerializer(Snippet.objects.all(), many=True) +>>> serializer.data +[{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}] +``` ## Using ModelSerializers @@ -186,23 +218,28 @@ In the same way that Django provides both `Form` classes and `ModelForm` classes Let's look at refactoring our serializer using the `ModelSerializer` class. Open the file `snippets/serializers.py` again, and replace the `SnippetSerializer` class with the following. - class SnippetSerializer(serializers.ModelSerializer): - class Meta: - model = Snippet - fields = ['id', 'title', 'code', 'linenos', 'language', 'style'] +```python +class SnippetSerializer(serializers.ModelSerializer): + class Meta: + model = Snippet + fields = ["id", "title", "code", "linenos", "language", "style"] +``` One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing its representation. Open the Django shell with `python manage.py shell`, then try the following: - from snippets.serializers import SnippetSerializer - serializer = SnippetSerializer() - print(repr(serializer)) - # SnippetSerializer(): - # id = IntegerField(label='ID', read_only=True) - # title = CharField(allow_blank=True, max_length=100, required=False) - # code = CharField(style={'base_template': 'textarea.html'}) - # linenos = BooleanField(required=False) - # language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')... - # style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')... +```pycon +>>> from snippets.serializers import SnippetSerializer + +>>> serializer = SnippetSerializer() +>>> print(repr(serializer)) +SnippetSerializer(): + id = IntegerField(label='ID', read_only=True) + title = CharField(allow_blank=True, max_length=100, required=False) + code = CharField(style={'base_template': 'textarea.html'}) + linenos = BooleanField(required=False) + language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')... + style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')... +``` It's important to remember that `ModelSerializer` classes don't do anything particularly magical, they are simply a shortcut for creating serializer classes: @@ -216,79 +253,89 @@ For the moment we won't use any of REST framework's other features, we'll just w Edit the `snippets/views.py` file, and add the following. - from django.http import HttpResponse, JsonResponse - from django.views.decorators.csrf import csrf_exempt - from rest_framework.parsers import JSONParser - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer +```python +from django.http import HttpResponse, JsonResponse +from django.views.decorators.csrf import csrf_exempt +from rest_framework.parsers import JSONParser +from snippets.models import Snippet +from snippets.serializers import SnippetSerializer +``` The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet. - @csrf_exempt - def snippet_list(request): - """ - List all code snippets, or create a new snippet. - """ - if request.method == 'GET': - snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets, many=True) - return JsonResponse(serializer.data, safe=False) +```python +@csrf_exempt +def snippet_list(request): + """ + List all code snippets, or create a new snippet. + """ + if request.method == "GET": + snippets = Snippet.objects.all() + serializer = SnippetSerializer(snippets, many=True) + return JsonResponse(serializer.data, safe=False) - elif request.method == 'POST': - data = JSONParser().parse(request) - serializer = SnippetSerializer(data=data) - if serializer.is_valid(): - serializer.save() - return JsonResponse(serializer.data, status=201) - return JsonResponse(serializer.errors, status=400) + elif request.method == "POST": + data = JSONParser().parse(request) + serializer = SnippetSerializer(data=data) + if serializer.is_valid(): + serializer.save() + return JsonResponse(serializer.data, status=201) + return JsonResponse(serializer.errors, status=400) +``` Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. - @csrf_exempt - def snippet_detail(request, pk): - """ - Retrieve, update or delete a code snippet. - """ - try: - snippet = Snippet.objects.get(pk=pk) - except Snippet.DoesNotExist: - return HttpResponse(status=404) +```python +@csrf_exempt +def snippet_detail(request, pk): + """ + Retrieve, update or delete a code snippet. + """ + try: + snippet = Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: + return HttpResponse(status=404) - if request.method == 'GET': - serializer = SnippetSerializer(snippet) + if request.method == "GET": + serializer = SnippetSerializer(snippet) + return JsonResponse(serializer.data) + + elif request.method == "PUT": + data = JSONParser().parse(request) + serializer = SnippetSerializer(snippet, data=data) + if serializer.is_valid(): + serializer.save() return JsonResponse(serializer.data) + return JsonResponse(serializer.errors, status=400) - elif request.method == 'PUT': - data = JSONParser().parse(request) - serializer = SnippetSerializer(snippet, data=data) - if serializer.is_valid(): - serializer.save() - return JsonResponse(serializer.data) - return JsonResponse(serializer.errors, status=400) - - elif request.method == 'DELETE': - snippet.delete() - return HttpResponse(status=204) + elif request.method == "DELETE": + snippet.delete() + return HttpResponse(status=204) +``` Finally we need to wire these views up. Create the `snippets/urls.py` file: - from django.urls import path - from snippets import views +```python +from django.urls import path +from snippets import views - urlpatterns = [ - path('snippets/', views.snippet_list), - path('snippets//', views.snippet_detail), - ] +urlpatterns = [ + path("snippets/", views.snippet_list), + path("snippets//", views.snippet_detail), +] +``` We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. - from django.urls import path, include +```python +from django.urls import path, include - urlpatterns = [ - path('', include('snippets.urls')), - ] +urlpatterns = [ + path("", include("snippets.urls")), +] +``` It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now. @@ -298,18 +345,22 @@ Now we can start up a sample server that serves our snippets. Quit out of the shell... - quit() +```pycon +>>> quit() +``` ...and start up Django's development server. - python manage.py runserver +```bash +python manage.py runserver - Validating models... +Validating models... - 0 errors found - Django version 5.0, using settings 'tutorial.settings' - Starting Development server at http://127.0.0.1:8000/ - Quit the server with CONTROL-C. +0 errors found +Django version 5.0, using settings 'tutorial.settings' +Starting Development server at http://127.0.0.1:8000/ +Quit the server with CONTROL-C. +``` In another terminal window, we can test the server. @@ -317,47 +368,26 @@ We can test our API using [curl][curl] or [httpie][httpie]. Httpie is a user fri You can install httpie using pip: - pip install httpie +```bash +pip install httpie +``` Finally, we can get a list of all of the snippets: - http GET http://127.0.0.1:8000/snippets/ --unsorted +```bash +http GET http://127.0.0.1:8000/snippets/ --unsorted - HTTP/1.1 200 OK - ... - [ - { - "id": 1, - "title": "", - "code": "foo = \"bar\"\n", - "linenos": false, - "language": "python", - "style": "friendly" - }, - { - "id": 2, - "title": "", - "code": "print(\"hello, world\")\n", - "linenos": false, - "language": "python", - "style": "friendly" - }, - { - "id": 3, - "title": "", - "code": "print(\"hello, world\")", - "linenos": false, - "language": "python", - "style": "friendly" - } - ] - -Or we can get a particular snippet by referencing its id: - - http GET http://127.0.0.1:8000/snippets/2/ --unsorted - - HTTP/1.1 200 OK - ... +HTTP/1.1 200 OK +... +[ + { + "id": 1, + "title": "", + "code": "foo = \"bar\"\n", + "linenos": false, + "language": "python", + "style": "friendly" + }, { "id": 2, "title": "", @@ -365,7 +395,34 @@ Or we can get a particular snippet by referencing its id: "linenos": false, "language": "python", "style": "friendly" + }, + { + "id": 3, + "title": "", + "code": "print(\"hello, world\")", + "linenos": false, + "language": "python", + "style": "friendly" } +] +``` + +Or we can get a particular snippet by referencing its id: + +```bash +http GET http://127.0.0.1:8000/snippets/2/ --unsorted + +HTTP/1.1 200 OK +... +{ + "id": 2, + "title": "", + "code": "print(\"hello, world\")\n", + "linenos": false, + "language": "python", + "style": "friendly" +} +``` Similarly, you can have the same json displayed by visiting these URLs in a web browser. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 47c7facfc..fceb118bd 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -7,14 +7,18 @@ Let's introduce a couple of essential building blocks. REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.data` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. - request.POST # Only handles form data. Only works for 'POST' method. - request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods. +```python +request.POST # Only handles form data. Only works for 'POST' method. +request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods. +``` ## Response objects REST framework also introduces a `Response` object, which is a type of `TemplateResponse` that takes unrendered content and uses content negotiation to determine the correct content type to return to the client. - return Response(data) # Renders to content type as requested by the client. +```python +return Response(data) # Renders to content type as requested by the client. +``` ## Status codes @@ -35,58 +39,62 @@ The wrappers also provide behavior such as returning `405 Method Not Allowed` re Okay, let's go ahead and start using these new components to refactor our views slightly. - from rest_framework import status - from rest_framework.decorators import api_view - from rest_framework.response import Response - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer +```python +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from snippets.models import Snippet +from snippets.serializers import SnippetSerializer - @api_view(['GET', 'POST']) - def snippet_list(request): - """ - List all code snippets, or create a new snippet. - """ - if request.method == 'GET': - snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets, many=True) - return Response(serializer.data) +@api_view(["GET", "POST"]) +def snippet_list(request): + """ + List all code snippets, or create a new snippet. + """ + if request.method == "GET": + snippets = Snippet.objects.all() + serializer = SnippetSerializer(snippets, many=True) + return Response(serializer.data) - elif request.method == 'POST': - serializer = SnippetSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + elif request.method == "POST": + serializer = SnippetSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +``` Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. Here is the view for an individual snippet, in the `views.py` module. - @api_view(['GET', 'PUT', 'DELETE']) - def snippet_detail(request, pk): - """ - Retrieve, update or delete a code snippet. - """ - try: - snippet = Snippet.objects.get(pk=pk) - except Snippet.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) +```python +@api_view(["GET", "PUT", "DELETE"]) +def snippet_detail(request, pk): + """ + Retrieve, update or delete a code snippet. + """ + try: + snippet = Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) - if request.method == 'GET': - serializer = SnippetSerializer(snippet) + if request.method == "GET": + serializer = SnippetSerializer(snippet) + return Response(serializer.data) + + elif request.method == "PUT": + serializer = SnippetSerializer(snippet, data=request.data) + if serializer.is_valid(): + serializer.save() return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - elif request.method == 'PUT': - serializer = SnippetSerializer(snippet, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - elif request.method == 'DELETE': - snippet.delete() - return Response(status=status.HTTP_204_NO_CONTENT) + elif request.method == "DELETE": + snippet.delete() + return Response(status=status.HTTP_204_NO_CONTENT) +``` This should all feel very familiar - it is not a lot different from working with regular Django views. @@ -94,28 +102,27 @@ Notice that we're no longer explicitly tying our requests or responses to a give ## Adding optional format suffixes to our URLs -To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url]. +To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [][json-url]. Start by adding a `format` keyword argument to both of the views, like so. - - def snippet_list(request, format=None): - +`def snippet_list(request, format=None):` and - - def snippet_detail(request, pk, format=None): +`def snippet_detail(request, pk, format=None):` Now update the `snippets/urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs. - from django.urls import path - from rest_framework.urlpatterns import format_suffix_patterns - from snippets import views +```python +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from snippets import views - urlpatterns = [ - path('snippets/', views.snippet_list), - path('snippets//', views.snippet_detail), - ] +urlpatterns = [ + path("snippets/", views.snippet_list), + path("snippets//", views.snippet_detail), +] - urlpatterns = format_suffix_patterns(urlpatterns) +urlpatterns = format_suffix_patterns(urlpatterns) +``` We don't necessarily need to add these extra url patterns in, but it gives us a simple, clean way of referring to a specific format. @@ -125,68 +132,76 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][ We can get a list of all of the snippets, as before. - http http://127.0.0.1:8000/snippets/ +```bash +http http://127.0.0.1:8000/snippets/ - HTTP/1.1 200 OK - ... - [ - { - "id": 1, - "title": "", - "code": "foo = \"bar\"\n", - "linenos": false, - "language": "python", - "style": "friendly" - }, - { - "id": 2, - "title": "", - "code": "print(\"hello, world\")\n", - "linenos": false, - "language": "python", - "style": "friendly" - } - ] +HTTP/1.1 200 OK +... +[ + { + "id": 1, + "title": "", + "code": "foo = \"bar\"\n", + "linenos": false, + "language": "python", + "style": "friendly" + }, + { + "id": 2, + "title": "", + "code": "print(\"hello, world\")\n", + "linenos": false, + "language": "python", + "style": "friendly" + } +] +``` We can control the format of the response that we get back, either by using the `Accept` header: - http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON - http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML +```bash +http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON +http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML +``` Or by appending a format suffix: - http http://127.0.0.1:8000/snippets.json # JSON suffix - http http://127.0.0.1:8000/snippets.api # Browsable API suffix +```bash +http http://127.0.0.1:8000/snippets.json # JSON suffix +http http://127.0.0.1:8000/snippets.api # Browsable API suffix +``` Similarly, we can control the format of the request that we send, using the `Content-Type` header. - # POST using form data - http --form POST http://127.0.0.1:8000/snippets/ code="print(123)" +```bash +# POST using form data +http --form POST http://127.0.0.1:8000/snippets/ code="print(123)" - { - "id": 3, - "title": "", - "code": "print(123)", - "linenos": false, - "language": "python", - "style": "friendly" - } +{ + "id": 3, + "title": "", + "code": "print(123)", + "linenos": false, + "language": "python", + "style": "friendly" +} - # POST using JSON - http --json POST http://127.0.0.1:8000/snippets/ code="print(456)" +# POST using JSON +http --json POST http://127.0.0.1:8000/snippets/ code="print(456)" - { - "id": 4, - "title": "", - "code": "print(456)", - "linenos": false, - "language": "python", - "style": "friendly" - } +{ + "id": 4, + "title": "", + "code": "print(456)", + "linenos": false, + "language": "python", + "style": "friendly" +} +``` If you add a `--debug` switch to the `http` requests above, you will be able to see the request type in request headers. -Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]. +Now go and open the API in a web browser, by visiting [][devserver]. ### Browsability diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index ccfcd095d..59aee8813 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -6,74 +6,82 @@ We can also write our API views using class-based views, rather than function ba We'll start by rewriting the root view as a class-based view. All this involves is a little bit of refactoring of `views.py`. - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer - from django.http import Http404 - from rest_framework.views import APIView - from rest_framework.response import Response - from rest_framework import status +```python +from snippets.models import Snippet +from snippets.serializers import SnippetSerializer +from django.http import Http404 +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status - class SnippetList(APIView): - """ - List all snippets, or create a new snippet. - """ - def get(self, request, format=None): - snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets, many=True) - return Response(serializer.data) +class SnippetList(APIView): + """ + List all snippets, or create a new snippet. + """ - def post(self, request, format=None): - serializer = SnippetSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def get(self, request, format=None): + snippets = Snippet.objects.all() + serializer = SnippetSerializer(snippets, many=True) + return Response(serializer.data) + + def post(self, request, format=None): + serializer = SnippetSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +``` So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. - class SnippetDetail(APIView): - """ - Retrieve, update or delete a snippet instance. - """ - def get_object(self, pk): - try: - return Snippet.objects.get(pk=pk) - except Snippet.DoesNotExist: - raise Http404 +```python +class SnippetDetail(APIView): + """ + Retrieve, update or delete a snippet instance. + """ - def get(self, request, pk, format=None): - snippet = self.get_object(pk) - serializer = SnippetSerializer(snippet) + def get_object(self, pk): + try: + return Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: + raise Http404 + + def get(self, request, pk, format=None): + snippet = self.get_object(pk) + serializer = SnippetSerializer(snippet) + return Response(serializer.data) + + def put(self, request, pk, format=None): + snippet = self.get_object(pk) + serializer = SnippetSerializer(snippet, data=request.data) + if serializer.is_valid(): + serializer.save() return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def put(self, request, pk, format=None): - snippet = self.get_object(pk) - serializer = SnippetSerializer(snippet, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, pk, format=None): - snippet = self.get_object(pk) - snippet.delete() - return Response(status=status.HTTP_204_NO_CONTENT) + def delete(self, request, pk, format=None): + snippet = self.get_object(pk) + snippet.delete() + return Response(status=status.HTTP_204_NO_CONTENT) +``` That's looking good. Again, it's still pretty similar to the function based view right now. We'll also need to refactor our `snippets/urls.py` slightly now that we're using class-based views. - from django.urls import path - from rest_framework.urlpatterns import format_suffix_patterns - from snippets import views +```python +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from snippets import views - urlpatterns = [ - path('snippets/', views.SnippetList.as_view()), - path('snippets//', views.SnippetDetail.as_view()), - ] +urlpatterns = [ + path("snippets/", views.SnippetList.as_view()), + path("snippets//", views.SnippetDetail.as_view()), +] - urlpatterns = format_suffix_patterns(urlpatterns) +urlpatterns = format_suffix_patterns(urlpatterns) +``` Okay, we're done. If you run the development server everything should be working just as before. @@ -85,42 +93,49 @@ The create/retrieve/update/delete operations that we've been using so far are go Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again. - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer - from rest_framework import mixins - from rest_framework import generics +```python +from snippets.models import Snippet +from snippets.serializers import SnippetSerializer +from rest_framework import mixins +from rest_framework import generics - class SnippetList(mixins.ListModelMixin, - mixins.CreateModelMixin, - generics.GenericAPIView): - queryset = Snippet.objects.all() - serializer_class = SnippetSerializer - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) +class SnippetList( + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView +): + queryset = Snippet.objects.all() + serializer_class = SnippetSerializer - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) +``` We'll take a moment to examine exactly what's happening here. We're building our view using `GenericAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`. The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. - class SnippetDetail(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView): - queryset = Snippet.objects.all() - serializer_class = SnippetSerializer +```python +class SnippetDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + queryset = Snippet.objects.all() + serializer_class = SnippetSerializer - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) +``` Pretty similar. Again we're using the `GenericAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions. @@ -128,19 +143,21 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down our `views.py` module even more. - from snippets.models import Snippet - from snippets.serializers import SnippetSerializer - from rest_framework import generics +```python +from snippets.models import Snippet +from snippets.serializers import SnippetSerializer +from rest_framework import generics - class SnippetList(generics.ListCreateAPIView): - queryset = Snippet.objects.all() - serializer_class = SnippetSerializer +class SnippetList(generics.ListCreateAPIView): + queryset = Snippet.objects.all() + serializer_class = SnippetSerializer - class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): - queryset = Snippet.objects.all() - serializer_class = SnippetSerializer +class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Snippet.objects.all() + serializer_class = SnippetSerializer +``` Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django. diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index cb0321ea2..cb5eef469 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -14,81 +14,103 @@ First, let's add a couple of fields. One of those fields will be used to repres Add the following two fields to the `Snippet` model in `models.py`. - owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) - highlighted = models.TextField() +```python +owner = models.ForeignKey( + "auth.User", related_name="snippets", on_delete=models.CASCADE +) +highlighted = models.TextField() +``` We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code highlighting library. We'll need some extra imports: - from pygments.lexers import get_lexer_by_name - from pygments.formatters.html import HtmlFormatter - from pygments import highlight +```python +from pygments.lexers import get_lexer_by_name +from pygments.formatters.html import HtmlFormatter +from pygments import highlight +``` And now we can add a `.save()` method to our model class: - def save(self, *args, **kwargs): - """ - Use the `pygments` library to create a highlighted HTML - representation of the code snippet. - """ - lexer = get_lexer_by_name(self.language) - linenos = 'table' if self.linenos else False - options = {'title': self.title} if self.title else {} - formatter = HtmlFormatter(style=self.style, linenos=linenos, - full=True, **options) - self.highlighted = highlight(self.code, lexer, formatter) - super().save(*args, **kwargs) +```python +def save(self, *args, **kwargs): + """ + Use the `pygments` library to create a highlighted HTML + representation of the code snippet. + """ + lexer = get_lexer_by_name(self.language) + linenos = "table" if self.linenos else False + options = {"title": self.title} if self.title else {} + formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options) + self.highlighted = highlight(self.code, lexer, formatter) + 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. - rm -f db.sqlite3 - rm -r snippets/migrations - python manage.py makemigrations snippets - python manage.py migrate +```bash +rm -f db.sqlite3 +rm -r snippets/migrations +python manage.py makemigrations snippets +python manage.py migrate +``` You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. - python manage.py createsuperuser +```bash +python manage.py createsuperuser +``` ## Adding endpoints for our User models Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add: - from django.contrib.auth.models import User +```python +from django.contrib.auth.models import User - class UserSerializer(serializers.ModelSerializer): - snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) - class Meta: - model = User - fields = ['id', 'username', 'snippets'] +class UserSerializer(serializers.ModelSerializer): + snippets = serializers.PrimaryKeyRelatedField( + many=True, queryset=Snippet.objects.all() + ) + + class Meta: + model = User + fields = ["id", "username", "snippets"] +``` Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it. We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class-based views. - from django.contrib.auth.models import User +```python +from django.contrib.auth.models import User - class UserList(generics.ListAPIView): - queryset = User.objects.all() - serializer_class = UserSerializer +class UserList(generics.ListAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer - class UserDetail(generics.RetrieveAPIView): - queryset = User.objects.all() - serializer_class = UserSerializer +class UserDetail(generics.RetrieveAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer +``` Make sure to also import the `UserSerializer` class - from snippets.serializers import UserSerializer +```python +from snippets.serializers import UserSerializer +``` Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `snippets/urls.py`. - path('users/', views.UserList.as_view()), - path('users//', views.UserDetail.as_view()), +```python +path("users/", views.UserList.as_view()), +path("users//", views.UserDetail.as_view()), +``` ## Associating Snippets with Users @@ -98,8 +120,10 @@ The way we deal with that is by overriding a `.perform_create()` method on our s On the `SnippetList` view class, add the following method: - def perform_create(self, serializer): - serializer.save(owner=self.request.user) +```python +def perform_create(self, serializer): + serializer.save(owner=self.request.user) +``` The `create()` method of our serializer will now be passed an additional `'owner'` field, along with the validated data from the request. @@ -107,7 +131,9 @@ The `create()` method of our serializer will now be passed an additional `'owner Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`: - owner = serializers.ReadOnlyField(source='owner.username') +```python +owner = serializers.ReadOnlyField(source="owner.username") +``` **Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class. @@ -123,11 +149,15 @@ REST framework includes a number of permission classes that we can use to restri First add the following import in the views module - from rest_framework import permissions +```python +from rest_framework import permissions +``` Then, add the following property to **both** the `SnippetList` and `SnippetDetail` view classes. - permission_classes = [permissions.IsAuthenticatedOrReadOnly] +```python +permission_classes = [permissions.IsAuthenticatedOrReadOnly] +``` ## Adding login to the Browsable API @@ -137,13 +167,17 @@ We can add a login view for use with the browsable API, by editing the URLconf i Add the following import at the top of the file: - from django.urls import path, include +```python +from django.urls import path, include +``` And, at the end of the file, add a pattern to include the login and logout views for the browsable API. - urlpatterns += [ - path('api-auth/', include('rest_framework.urls')), - ] +```python +urlpatterns += [ + path("api-auth/", include("rest_framework.urls")), +] +``` The `'api-auth/'` part of pattern can actually be whatever URL you want to use. @@ -159,31 +193,36 @@ To do that we're going to need to create a custom permission. In the snippets app, create a new file, `permissions.py` - from rest_framework import permissions +```python +from rest_framework import permissions - class IsOwnerOrReadOnly(permissions.BasePermission): - """ - Custom permission to only allow owners of an object to edit it. - """ +class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ - def has_object_permission(self, request, view, obj): - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True - # Write permissions are only allowed to the owner of the snippet. - return obj.owner == request.user + # Write permissions are only allowed to the owner of the snippet. + return obj.owner == request.user +``` Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` view class: - permission_classes = [permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrReadOnly] +```python +permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] +``` Make sure to also import the `IsOwnerOrReadOnly` class. - from snippets.permissions import IsOwnerOrReadOnly +```python +from snippets.permissions import IsOwnerOrReadOnly +``` Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions only appear on a snippet instance endpoint if you're logged in as the same user that created the code snippet. @@ -197,25 +236,29 @@ If we're interacting with the API programmatically we need to explicitly provide If we try to create a snippet without authenticating, we'll get an error: - http POST http://127.0.0.1:8000/snippets/ code="print(123)" +```bash +http POST http://127.0.0.1:8000/snippets/ code="print(123)" - { - "detail": "Authentication credentials were not provided." - } +{ + "detail": "Authentication credentials were not provided." +} +``` We can make a successful request by including the username and password of one of the users we created earlier. - http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)" +```bash +http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)" - { - "id": 1, - "owner": "admin", - "title": "foo", - "code": "print(789)", - "linenos": false, - "language": "python", - "style": "friendly" - } +{ + "id": 1, + "owner": "admin", + "title": "foo", + "code": "print(789)", + "linenos": false, + "language": "python", + "style": "friendly" +} +``` ## Summary diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index f999fdf50..3800c168b 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -6,17 +6,21 @@ At the moment relationships within our API are represented by using primary keys Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add: - from rest_framework.decorators import api_view - from rest_framework.response import Response - from rest_framework.reverse import reverse +```python +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.reverse import reverse - @api_view(['GET']) - def api_root(request, format=None): - return Response({ - 'users': reverse('user-list', request=request, format=format), - 'snippets': reverse('snippet-list', request=request, format=format) - }) +@api_view(["GET"]) +def api_root(request, format=None): + return Response( + { + "users": reverse("user-list", request=request, format=format), + "snippets": reverse("snippet-list", request=request, format=format), + } + ) +``` Two things should be noticed here. First, we're using REST framework's `reverse` function in order to return fully-qualified URLs; second, URL patterns are identified by convenience names that we will declare later on in our `snippets/urls.py`. @@ -30,24 +34,31 @@ 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 +```python +from rest_framework import renderers - class SnippetHighlight(generics.GenericAPIView): - queryset = Snippet.objects.all() - renderer_classes = [renderers.StaticHTMLRenderer] - def get(self, request, *args, **kwargs): - snippet = self.get_object() - return Response(snippet.highlighted) +class SnippetHighlight(generics.GenericAPIView): + queryset = Snippet.objects.all() + renderer_classes = [renderers.StaticHTMLRenderer] + + def get(self, request, *args, **kwargs): + snippet = self.get_object() + return Response(snippet.highlighted) +``` As usual we need to add the new views that we've created in to our URLconf. We'll add a url pattern for our new API root in `snippets/urls.py`: - path('', views.api_root), +```python +path("", views.api_root), +``` And then add a url pattern for the snippet highlights: - path('snippets//highlight/', views.SnippetHighlight.as_view()), +```python +path("snippets//highlight/", views.SnippetHighlight.as_view()), +``` ## Hyperlinking our API @@ -73,27 +84,62 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial We can easily re-write our existing serializers to use hyperlinking. In your `snippets/serializers.py` add: - class SnippetSerializer(serializers.HyperlinkedModelSerializer): - owner = serializers.ReadOnlyField(source='owner.username') - highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') +```python +class SnippetSerializer(serializers.HyperlinkedModelSerializer): + owner = serializers.ReadOnlyField(source="owner.username") + highlight = serializers.HyperlinkedIdentityField( + view_name="snippet-highlight", format="html" + ) - class Meta: - model = Snippet - fields = ['url', 'id', 'highlight', 'owner', - 'title', 'code', 'linenos', 'language', 'style'] + class Meta: + model = Snippet + fields = [ + "url", + "id", + "highlight", + "owner", + "title", + "code", + "linenos", + "language", + "style", + ] - class UserSerializer(serializers.HyperlinkedModelSerializer): - snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True) +class UserSerializer(serializers.HyperlinkedModelSerializer): + snippets = serializers.HyperlinkedRelatedField( + many=True, view_name="snippet-detail", read_only=True + ) - class Meta: - model = User - fields = ['url', 'id', 'username', 'snippets'] + class Meta: + model = User + fields = ["url", "id", "username", "snippets"] +``` Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern. Because we've included format suffixed URLs such as `'.json'`, we also need to indicate on the `highlight` field that any format suffixed hyperlinks it returns should use the `'.html'` suffix. +--- + +**Note:** + +When you are manually instantiating these serializers inside your views (e.g., in `SnippetDetail` or `SnippetList`), you **must** pass `context={'request': request}` so the serializer knows how to build absolute URLs. For example, instead of: + +```python +serializer = SnippetSerializer(snippet) +``` + +You must write: + +```python +serializer = SnippetSerializer(snippet, context={"request": request}) +``` + +If your view is a subclass of `GenericAPIView`, you may use the `get_serializer_context()` as a convenience method. + +--- + ## Making sure our URL patterns are named If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name. @@ -105,29 +151,29 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p After adding all those names into our URLconf, our final `snippets/urls.py` file should look like this: - from django.urls import path - from rest_framework.urlpatterns import format_suffix_patterns - from snippets import views +```python +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from snippets import views - # API endpoints - urlpatterns = format_suffix_patterns([ - path('', views.api_root), - path('snippets/', - views.SnippetList.as_view(), - name='snippet-list'), - path('snippets//', - views.SnippetDetail.as_view(), - name='snippet-detail'), - path('snippets//highlight/', +# API endpoints +urlpatterns = format_suffix_patterns( + [ + path("", views.api_root), + path("snippets/", views.SnippetList.as_view(), name="snippet-list"), + path( + "snippets//", views.SnippetDetail.as_view(), name="snippet-detail" + ), + path( + "snippets//highlight/", views.SnippetHighlight.as_view(), - name='snippet-highlight'), - path('users/', - views.UserList.as_view(), - name='user-list'), - path('users//', - views.UserDetail.as_view(), - name='user-detail') - ]) + name="snippet-highlight", + ), + path("users/", views.UserList.as_view(), name="user-list"), + path("users//", views.UserDetail.as_view(), name="user-detail"), + ] +) +``` ## Adding pagination @@ -135,10 +181,12 @@ The list views for users and code snippets could end up returning quite a lot of We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting: - REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 10 - } +```python +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, +} +``` 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. diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 6fa2111e7..2c6760a54 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -12,45 +12,50 @@ Let's take our current set of views, and refactor them into view sets. First of all let's refactor our `UserList` and `UserDetail` classes into a single `UserViewSet` class. In the `snippets/views.py` file, we can remove the two view classes and replace them with a single ViewSet class: - from rest_framework import viewsets +```python +from rest_framework import viewsets - class UserViewSet(viewsets.ReadOnlyModelViewSet): - """ - This viewset automatically provides `list` and `retrieve` actions. - """ - queryset = User.objects.all() - serializer_class = UserSerializer +class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + This viewset automatically provides `list` and `retrieve` actions. + """ + + queryset = User.objects.all() + serializer_class = UserSerializer +``` Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes. Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. - from rest_framework import permissions - from rest_framework import renderers - from rest_framework.decorators import action - from rest_framework.response import Response +```python +from rest_framework import permissions +from rest_framework import renderers +from rest_framework.decorators import action +from rest_framework.response import Response - class SnippetViewSet(viewsets.ModelViewSet): - """ - This ViewSet automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. +class SnippetViewSet(viewsets.ModelViewSet): + """ + This ViewSet automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. - Additionally we also provide an extra `highlight` action. - """ - queryset = Snippet.objects.all() - serializer_class = SnippetSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrReadOnly] + Additionally we also provide an extra `highlight` action. + """ - @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer]) - def highlight(self, request, *args, **kwargs): - snippet = self.get_object() - return Response(snippet.highlighted) + queryset = Snippet.objects.all() + serializer_class = SnippetSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] - def perform_create(self, serializer): - serializer.save(owner=self.request.user) + @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer]) + def highlight(self, request, *args, **kwargs): + snippet = self.get_object() + return Response(snippet.highlighted) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) +``` This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. @@ -67,42 +72,40 @@ To see what's going on under the hood let's first explicitly create a set of vie In the `snippets/urls.py` file we bind our `ViewSet` classes into a set of concrete views. - from rest_framework import renderers +```python +from rest_framework import renderers - from snippets.views import api_root, SnippetViewSet, UserViewSet +from snippets.views import api_root, SnippetViewSet, UserViewSet - snippet_list = SnippetViewSet.as_view({ - 'get': 'list', - 'post': 'create' - }) - snippet_detail = SnippetViewSet.as_view({ - 'get': 'retrieve', - 'put': 'update', - 'patch': 'partial_update', - 'delete': 'destroy' - }) - snippet_highlight = SnippetViewSet.as_view({ - 'get': 'highlight' - }, renderer_classes=[renderers.StaticHTMLRenderer]) - user_list = UserViewSet.as_view({ - 'get': 'list' - }) - user_detail = UserViewSet.as_view({ - 'get': 'retrieve' - }) +snippet_list = SnippetViewSet.as_view({"get": "list", "post": "create"}) +snippet_detail = SnippetViewSet.as_view( + {"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"} +) +snippet_highlight = SnippetViewSet.as_view( + {"get": "highlight"}, renderer_classes=[renderers.StaticHTMLRenderer] +) +user_list = UserViewSet.as_view({"get": "list"}) +user_detail = UserViewSet.as_view({"get": "retrieve"}) +``` Notice how we're creating multiple views from each `ViewSet` class, by binding the HTTP methods to the required action for each view. Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual. - urlpatterns = format_suffix_patterns([ - path('', api_root), - path('snippets/', snippet_list, name='snippet-list'), - path('snippets//', snippet_detail, name='snippet-detail'), - path('snippets//highlight/', snippet_highlight, name='snippet-highlight'), - path('users/', user_list, name='user-list'), - path('users//', user_detail, name='user-detail') - ]) +```python +urlpatterns = format_suffix_patterns( + [ + path("", api_root), + path("snippets/", snippet_list, name="snippet-list"), + path("snippets//", snippet_detail, name="snippet-detail"), + path( + "snippets//highlight/", snippet_highlight, name="snippet-highlight" + ), + path("users/", user_list, name="user-list"), + path("users//", user_detail, name="user-detail"), + ] +) +``` ## Using Routers @@ -110,20 +113,22 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do Here's our re-wired `snippets/urls.py` file. - from django.urls import path, include - from rest_framework.routers import DefaultRouter +```python +from django.urls import path, include +from rest_framework.routers import DefaultRouter - from snippets import views +from snippets import views - # Create a router and register our ViewSets with it. - router = DefaultRouter() - router.register(r'snippets', views.SnippetViewSet, basename='snippet') - router.register(r'users', views.UserViewSet, basename='user') +# Create a router and register our ViewSets with it. +router = DefaultRouter() +router.register(r"snippets", views.SnippetViewSet, basename="snippet") +router.register(r"users", views.UserViewSet, basename="user") - # The API URLs are now determined automatically by the router. - urlpatterns = [ - path('', include(router.urls)), - ] +# The API URLs are now determined automatically by the router. +urlpatterns = [ + path("", include(router.urls)), +] +``` Registering the ViewSets with the router is similar to providing a urlpattern. We include two arguments - the URL prefix for the views, and the view set itself. diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index a140dbce0..f0f6e6c4b 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -6,57 +6,65 @@ We're going to create a simple API to allow admin users to view and edit the use Create a new Django project named `tutorial`, then start a new app called `quickstart`. - # Create the project directory - mkdir tutorial - cd tutorial +```bash +# Create the project directory +mkdir tutorial +cd tutorial - # Create a virtual environment to isolate our package dependencies locally - python3 -m venv env - source env/bin/activate # On Windows use `env\Scripts\activate` +# Create a virtual environment to isolate our package dependencies locally +python3 -m venv env +source env/bin/activate # On Windows use `env\Scripts\activate` - # Install Django and Django REST framework into the virtual environment - pip install djangorestframework +# Install Django and Django REST framework into the virtual environment +pip install djangorestframework - # Set up a new project with a single application - django-admin startproject tutorial . # Note the trailing '.' character - cd tutorial - django-admin startapp quickstart - cd .. +# Set up a new project with a single application +django-admin startproject tutorial . # Note the trailing '.' character +cd tutorial +django-admin startapp quickstart +cd .. +``` The project layout should look like: - $ pwd - /tutorial - $ find . - . - ./tutorial - ./tutorial/asgi.py - ./tutorial/__init__.py - ./tutorial/quickstart - ./tutorial/quickstart/migrations - ./tutorial/quickstart/migrations/__init__.py - ./tutorial/quickstart/models.py - ./tutorial/quickstart/__init__.py - ./tutorial/quickstart/apps.py - ./tutorial/quickstart/admin.py - ./tutorial/quickstart/tests.py - ./tutorial/quickstart/views.py - ./tutorial/settings.py - ./tutorial/urls.py - ./tutorial/wsgi.py - ./env - ./env/... - ./manage.py +```bash +$ pwd +/tutorial +$ find . +. +./tutorial +./tutorial/asgi.py +./tutorial/__init__.py +./tutorial/quickstart +./tutorial/quickstart/migrations +./tutorial/quickstart/migrations/__init__.py +./tutorial/quickstart/models.py +./tutorial/quickstart/__init__.py +./tutorial/quickstart/apps.py +./tutorial/quickstart/admin.py +./tutorial/quickstart/tests.py +./tutorial/quickstart/views.py +./tutorial/settings.py +./tutorial/urls.py +./tutorial/wsgi.py +./env +./env/... +./manage.py +``` It may look unusual that the application has been created within the project directory. Using the project's namespace avoids name clashes with external modules (a topic that goes outside the scope of the quickstart). Now sync your database for the first time: - python manage.py migrate +```bash +python manage.py migrate +``` We'll also create an initial user named `admin` with a password. We'll authenticate as that user later in our example. - python manage.py createsuperuser --username admin --email admin@example.com +```bash +python manage.py createsuperuser --username admin --email admin@example.com +``` Once you've set up a database and the initial user is created and ready to go, open up the app's directory and we'll get coding... @@ -64,20 +72,22 @@ Once you've set up a database and the initial user is created and ready to go, o First up we're going to define some serializers. Let's create a new module named `tutorial/quickstart/serializers.py` that we'll use for our data representations. - from django.contrib.auth.models import Group, User - from rest_framework import serializers +```python +from django.contrib.auth.models import Group, User +from rest_framework import serializers - class UserSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = User - fields = ['url', 'username', 'email', 'groups'] +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ["url", "username", "email", "groups"] - class GroupSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = Group - fields = ['url', 'name'] +class GroupSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Group + fields = ["url", "name"] +``` Notice that we're using hyperlinked relations in this case with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design. @@ -85,28 +95,32 @@ Notice that we're using hyperlinked relations in this case with `HyperlinkedMode Right, we'd better write some views then. Open `tutorial/quickstart/views.py` and get typing. - from django.contrib.auth.models import Group, User - from rest_framework import permissions, viewsets +```python +from django.contrib.auth.models import Group, User +from rest_framework import permissions, viewsets - from tutorial.quickstart.serializers import GroupSerializer, UserSerializer +from tutorial.quickstart.serializers import GroupSerializer, UserSerializer - class UserViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows users to be viewed or edited. - """ - queryset = User.objects.all().order_by('-date_joined') - serializer_class = UserSerializer - permission_classes = [permissions.IsAuthenticated] +class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + + queryset = User.objects.all().order_by("-date_joined") + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] - class GroupViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows groups to be viewed or edited. - """ - queryset = Group.objects.all().order_by('name') - serializer_class = GroupSerializer - permission_classes = [permissions.IsAuthenticated] +class GroupViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows groups to be viewed or edited. + """ + + queryset = Group.objects.all().order_by("name") + serializer_class = GroupSerializer + permission_classes = [permissions.IsAuthenticated] +``` Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. @@ -116,21 +130,23 @@ We can easily break these down into individual views if we need to, but using vi Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... - from django.urls import include, path - from rest_framework import routers +```python +from django.urls import include, path +from rest_framework import routers - from tutorial.quickstart import views +from tutorial.quickstart import views - router = routers.DefaultRouter() - router.register(r'users', views.UserViewSet) - router.register(r'groups', views.GroupViewSet) +router = routers.DefaultRouter() +router.register(r"users", views.UserViewSet) +router.register(r"groups", views.GroupViewSet) - # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browsable API. - urlpatterns = [ - path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) - ] +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + path("", include(router.urls)), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), +] +``` Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class. @@ -139,21 +155,26 @@ Again, if we need more control over the API URLs we can simply drop down to usin Finally, we're including default login and logout views for use with the browsable API. That's optional, but useful if your API requires authentication and you want to use the browsable API. ## Pagination + Pagination allows you to control how many objects per page are returned. To enable it add the following lines to `tutorial/settings.py` - REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 10 - } +```python +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, +} +``` ## Settings Add `'rest_framework'` to `INSTALLED_APPS`. The settings module will be in `tutorial/settings.py` - INSTALLED_APPS = [ - ... - 'rest_framework', - ] +```text +INSTALLED_APPS = [ + ... + 'rest_framework', +] +``` Okay, we're done. @@ -163,46 +184,51 @@ Okay, we're done. We're now ready to test the API we've built. Let's fire up the server from the command line. - python manage.py runserver +```bash +python manage.py runserver +``` We can now access our API, both from the command-line, using tools like `curl`... - bash: curl -u admin -H 'Accept: application/json; indent=4' http://127.0.0.1:8000/users/ - Enter host password for user 'admin': - { - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "url": "http://127.0.0.1:8000/users/1/", - "username": "admin", - "email": "admin@example.com", - "groups": [] - } - ] - } +```bash +bash: curl -u admin -H 'Accept: application/json; indent=4' http://127.0.0.1:8000/users/ +Enter host password for user 'admin': +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "url": "http://127.0.0.1:8000/users/1/", + "username": "admin", + "email": "admin@example.com", + "groups": [] + } + ] +} +``` Or using the [httpie][httpie], command line tool... - bash: http -a admin http://127.0.0.1:8000/users/ - http: password for admin@127.0.0.1:8000:: - $HTTP/1.1 200 OK - ... - { - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "email": "admin@example.com", - "groups": [], - "url": "http://127.0.0.1:8000/users/1/", - "username": "admin" - } - ] - } - +```bash +bash: http -a admin http://127.0.0.1:8000/users/ +http: password for admin@127.0.0.1:8000:: +$HTTP/1.1 200 OK +... +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "email": "admin@example.com", + "groups": [], + "url": "http://127.0.0.1:8000/users/1/", + "username": "admin" + } + ] +} +``` Or directly through the browser, by going to the URL `http://127.0.0.1:8000/users/`... diff --git a/docs_theme/main.html b/docs_theme/main.html index b4e894781..e37309595 100644 --- a/docs_theme/main.html +++ b/docs_theme/main.html @@ -110,7 +110,7 @@ {% block content %} {% if page.meta.source %} {% for filename in page.meta.source %} - + {{ filename }} {% endfor %} diff --git a/docs_theme/nav.html b/docs_theme/nav.html index d30348756..df2fd97d0 100644 --- a/docs_theme/nav.html +++ b/docs_theme/nav.html @@ -1,7 +1,7 @@