Merge branch 'encode:master' into master

This commit is contained in:
Yousef Abu Shanab 2024-02-07 08:21:05 -08:00 committed by GitHub
commit 516b5db8ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 238 additions and 138 deletions

View File

@ -22,7 +22,7 @@ jobs:
- '3.11'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@ -9,12 +9,19 @@ repos:
- id: check-symlinks
- id: check-toml
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 3.9.0
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies:
- flake8-tidy-imports
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
hooks:
- id: blacken-docs
exclude: ^(?!docs).*$
additional_dependencies:
- black==23.1.0

View File

@ -1,6 +1,6 @@
# Contributing to REST framework
At this point in it's lifespan we consider Django REST framework to be essentially feature-complete. We may accept pull requests that track the continued development of Django versions, but would prefer not to accept new features or code formatting changes.
At this point in its lifespan we consider Django REST framework to be 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.

View File

@ -27,8 +27,9 @@ The initial aim is to provide a single full-time position on REST framework.
[![][posthog-img]][posthog-url]
[![][cryptapi-img]][cryptapi-url]
[![][fezto-img]][fezto-url]
[![][svix-img]][svix-url]
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], and [FEZTO][fezto-url].
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], [FEZTO][fezto-url], and [Svix][svix-url].
---
@ -38,7 +39,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
* The [Web browsable API][sandbox] is a huge usability win for your developers.
* The Web browsable API is a huge usability win for your developers.
* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
@ -200,6 +201,7 @@ Please see the [security policy][security-policy].
[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
[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
@ -209,6 +211,7 @@ Please see the [security policy][security-policy].
[posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship
[cryptapi-url]: https://cryptapi.io
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
[svix-url]: https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
[oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit

View File

@ -454,7 +454,7 @@ More information can be found in the [Documentation](https://django-rest-durin.r
[basicauth]: https://tools.ietf.org/html/rfc2617
[permission]: permissions.md
[throttling]: throttling.md
[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax
[csrf-ajax]: https://docs.djangoproject.com/en/stable/howto/csrf/#using-csrf-protection-with-ajax
[mod_wsgi_official]: https://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIPassAuthorization.html
[django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html
[django-rest-framework-oauth]: https://jpadilla.github.io/django-rest-framework-oauth/

View File

@ -28,33 +28,33 @@ from rest_framework import viewsets
class UserViewSet(viewsets.ViewSet):
# With cookie: cache requested url for each user for 2 hours
@method_decorator(cache_page(60*60*2))
@method_decorator(cache_page(60 * 60 * 2))
@method_decorator(vary_on_cookie)
def list(self, request, format=None):
content = {
'user_feed': request.user.get_user_feed()
"user_feed": request.user.get_user_feed(),
}
return Response(content)
class ProfileView(APIView):
# With auth: cache requested url for each user for 2 hours
@method_decorator(cache_page(60*60*2))
@method_decorator(vary_on_headers("Authorization",))
@method_decorator(cache_page(60 * 60 * 2))
@method_decorator(vary_on_headers("Authorization"))
def get(self, request, format=None):
content = {
'user_feed': request.user.get_user_feed()
"user_feed": request.user.get_user_feed(),
}
return Response(content)
class PostView(APIView):
# Cache page for the requested url
@method_decorator(cache_page(60*60*2))
@method_decorator(cache_page(60 * 60 * 2))
def get(self, request, format=None):
content = {
'title': 'Post title',
'body': 'Post content'
"title": "Post title",
"body": "Post content",
}
return Response(content)
```

View File

@ -303,7 +303,7 @@ Corresponds to `django.db.models.fields.DecimalField`.
* `min_value` Validate that the number provided is no less than this value.
* `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
* `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without loosing data. Defaults to `False`.
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`.
#### Example usage

View File

@ -94,11 +94,13 @@ urlpatterns = [
# Use the `get_schema_view()` helper to add a `SchemaView` to project URLs.
# * `title` and `description` parameters are passed to `SchemaGenerator`.
# * Provide view name for use with `reverse()`.
path('openapi', get_schema_view(
title="Your Project",
description="API for all things …",
version="1.0.0"
), name='openapi-schema'),
path(
"openapi",
get_schema_view(
title="Your Project", description="API for all things …", version="1.0.0"
),
name="openapi-schema",
),
# ...
]
```
@ -259,11 +261,13 @@ class CustomSchema(AutoSchema):
"""
AutoSchema subclass using schema_extra_info on the view.
"""
...
class CustomView(APIView):
schema = CustomSchema()
schema_extra_info = ... some extra info ...
schema_extra_info = ... # some extra info
```
Here, the `AutoSchema` subclass goes looking for `schema_extra_info` on the
@ -278,10 +282,13 @@ class BaseSchema(AutoSchema):
"""
AutoSchema subclass that knows how to use extra_info.
"""
...
class CustomSchema(BaseSchema):
extra_info = ... some extra info ...
extra_info = ... # some extra info
class CustomView(APIView):
schema = CustomSchema()
@ -302,10 +309,9 @@ class CustomSchema(BaseSchema):
self.extra_info = kwargs.pop("extra_info")
super().__init__(**kwargs)
class CustomView(APIView):
schema = CustomSchema(
extra_info=... some extra info ...
)
schema = CustomSchema(extra_info=...) # some extra info
```
This saves you having to create a custom subclass per-view for a commonly used option.

View File

@ -163,6 +163,12 @@ The string that should used for any versioning parameters, such as in the media
Default: `'version'`
#### DEFAULT_VERSIONING_CLASS
The default versioning scheme to use.
Default: `None`
---
## Authentication settings

View File

@ -173,11 +173,9 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
# Advanced field defaults
Validators that are applied across multiple fields in the serializer can sometimes require a field input that should not be provided by the API client, but that *is* available as input to the validator.
For this purposes use `HiddenField`. This field will be present in `validated_data` but *will not* be used in the serializer output representation.
Two patterns that you may want to use for this sort of validation include:
* Using `HiddenField`. This field will be present in `validated_data` but *will not* be used in the serializer output representation.
* Using a standard field with `read_only=True`, but that also includes a `default=…` argument. This field *will* be used in the serializer output representation, but cannot be set directly by the user.
**Note:** Using a `read_only=True` field is excluded from writable fields so it won't use a `default=…` argument. Look [3.8 announcement](https://www.django-rest-framework.org/community/3.8-announcement/#altered-the-behaviour-of-read_only-plus-default-on-field).
REST framework includes a couple of defaults that may be useful in this context.
@ -189,7 +187,7 @@ A default class that can be used to represent the current user. In order to use
default=serializers.CurrentUserDefault()
)
#### CreateOnlyDefault
#### CreateOnlyDefault
A default class that can be used to *only set a default argument during create operations*. During updates the field is omitted.

View File

@ -201,15 +201,16 @@ To view all extra actions, call the `.get_extra_actions()` method.
Extra actions can map additional HTTP methods to separate `ViewSet` methods. For example, the above password set/unset methods could be consolidated into a single route. Note that additional mappings do not accept arguments.
```python
@action(detail=True, methods=['put'], name='Change Password')
def password(self, request, pk=None):
"""Update the user's password."""
...
@action(detail=True, methods=["put"], name="Change Password")
def password(self, request, pk=None):
"""Update the user's password."""
...
@password.mapping.delete
def delete_password(self, request, pk=None):
"""Delete the user's password."""
...
@password.mapping.delete
def delete_password(self, request, pk=None):
"""Delete the user's password."""
...
```
## Reversing action URLs
@ -220,14 +221,14 @@ Note that the `basename` is provided by the router during `ViewSet` registration
Using the example from the previous section:
```python
>>> view.reverse_action('set-password', args=['1'])
```pycon
>>> view.reverse_action("set-password", args=["1"])
'http://localhost:8000/api/users/1/set_password'
```
Alternatively, you can use the `url_name` attribute set by the `@action` decorator.
```python
```pycon
>>> view.reverse_action(view.set_password.url_name, args=['1'])
'http://localhost:8000/api/users/1/set_password'
```
@ -310,7 +311,7 @@ You may need to provide custom `ViewSet` classes that do not have the full set o
To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions:
from rest_framework import mixins
from rest_framework import mixins, viewsets
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,

View File

@ -41,8 +41,8 @@ update your REST framework settings to include `DEFAULT_SCHEMA_CLASS` explicitly
```python
REST_FRAMEWORK = {
...
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
...: ...,
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
}
```
@ -74,10 +74,11 @@ urlpatterns = [
# Use the `get_schema_view()` helper to add a `SchemaView` to project URLs.
# * `title` and `description` parameters are passed to `SchemaGenerator`.
# * Provide view name for use with `reverse()`.
path('openapi', get_schema_view(
title="Your Project",
description="API for all things …"
), name='openapi-schema'),
path(
"openapi",
get_schema_view(title="Your Project", description="API for all things …"),
name="openapi-schema",
),
# ...
]
```

View File

@ -43,10 +43,11 @@ be extracted from the class docstring:
```python
class DocStringExampleListView(APIView):
"""
get: A description of my GET operation.
post: A description of my POST operation.
"""
"""
get: A description of my GET operation.
post: A description of my POST operation.
"""
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get(self, request, *args, **kwargs):
@ -63,7 +64,7 @@ In some circumstances a Validator class or a Default class may need to access th
* Uniqueness validators need to be able to determine the name of the field to which they are applied, in order to run an appropriate database query.
* The `CurrentUserDefault` needs to be able to determine the context with which the serializer was instantiated, in order to return the current user instance.
Previous our approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13.
Our previous approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13.
Instead, validators or defaults which require the serializer context, should include a `requires_context = True` attribute on the class.

View File

@ -41,7 +41,7 @@ The tags used for a particular view may also be overridden...
```python
class MyOrders(APIView):
schema = AutoSchema(tags=['users', 'orders'])
schema = AutoSchema(tags=["users", "orders"])
...
```
@ -68,7 +68,7 @@ may be overridden if needed](https://www.django-rest-framework.org/api-guide/sch
```python
class MyOrders(APIView):
schema = AutoSchema(component_name="OrderDetails")
schema = AutoSchema(component_name="OrderDetails")
```
## More Public API
@ -118,10 +118,11 @@ class SitesSearchView(generics.ListAPIView):
by a search against the site name or location. (Location searches are
matched against the region and country names.)
"""
queryset = Sites.objects.all()
serializer_class = SitesSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['site_name', 'location__region', 'location__country']
search_fields = ["site_name", "location__region", "location__country"]
```
### Searches against annotate fields
@ -135,10 +136,11 @@ class PublisherSearchView(generics.ListAPIView):
Search for publishers, optionally filtering the search against the average
rating of all their books.
"""
queryset = Publisher.objects.annotate(avg_rating=Avg('book__rating'))
queryset = Publisher.objects.annotate(avg_rating=Avg("book__rating"))
serializer_class = PublisherSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['avg_rating']
search_fields = ["avg_rating"]
```
---

View File

@ -64,14 +64,10 @@ from rest_framework.schemas import get_schema_view
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
schema_view = get_schema_view(
title='Example API',
renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]
title="Example API", renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]
)
urlpatterns = [
path('swagger/', schema_view),
...
]
urlpatterns = [path("swagger/", schema_view), ...]
```
There have been a large number of fixes to the schema generation. These should

View File

@ -65,15 +65,12 @@ from rest_framework.renderers import JSONOpenAPIRenderer
from django.urls import path
schema_view = get_schema_view(
title='Server Monitoring API',
url='https://www.example.org/api/',
renderer_classes=[JSONOpenAPIRenderer]
title="Server Monitoring API",
url="https://www.example.org/api/",
renderer_classes=[JSONOpenAPIRenderer],
)
urlpatterns = [
path('schema.json', schema_view),
...
]
urlpatterns = [path("schema.json", schema_view), ...]
```
And here's how you can use the `generateschema` management command:

View File

@ -47,7 +47,7 @@ Date: 22nd September 2022
* Stop calling `set_context` on Validators. [[#8589](https://github.com/encode/django-rest-framework/pull/8589)]
* Return `NotImplemented` from `ErrorDetails.__ne__`. [[#8538](https://github.com/encode/django-rest-framework/pull/8538)]
* Don't evaluate `DateTimeField.default_timezone` when a custom timezone is set. [[#8531](https://github.com/encode/django-rest-framework/pull/8531)]
* Make relative URLs clickable in Browseable API. [[#8464](https://github.com/encode/django-rest-framework/pull/8464)]
* Make relative URLs clickable in Browsable API. [[#8464](https://github.com/encode/django-rest-framework/pull/8464)]
* Support `ManyRelatedField` falling back to the default value when the attribute specified by dot notation doesn't exist. Matches `ManyRelatedField.get_attribute` to `Field.get_attribute`. [[#7574](https://github.com/encode/django-rest-framework/pull/7574)]
* Make `schemas.openapi.get_reference` public. [[#7515](https://github.com/encode/django-rest-framework/pull/7515)]
* Make `ReturnDict` support `dict` union operators on Python 3.9 and later. [[#8302](https://github.com/encode/django-rest-framework/pull/8302)]
@ -65,7 +65,7 @@ Date: 15th December 2021
Date: 13th December 2021
* Django 4.0 compatability. [#8178]
* Django 4.0 compatibility. [#8178]
* Add `max_length` and `min_length` options to `ListSerializer`. [#8165]
* Add `get_request_serializer` and `get_response_serializer` hooks to `AutoSchema`. [#7424]
* Fix OpenAPI representation of null-able read only fields. [#8116]
@ -306,7 +306,11 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10.
class NullableCharField(serializers.CharField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.validators = [v for v in self.validators if not isinstance(v, ProhibitNullCharactersValidator)]
self.validators = [
v
for v in self.validators
if not isinstance(v, ProhibitNullCharactersValidator)
]
```
* Add `OpenAPIRenderer` and `generate_schema` management command. [#6229][gh6229]
* Add OpenAPIRenderer by default, and add schema docs. [#6233][gh6233]
@ -950,7 +954,7 @@ See the [release announcement][3.6-release].
* Prevent raising exception when limit is 0. ([#4098][gh4098])
* TokenAuthentication: Allow custom keyword in the header. ([#4097][gh4097])
* Handle incorrectly padded HTTP basic auth header. ([#4090][gh4090])
* LimitOffset pagination crashes Browseable API when limit=0. ([#4079][gh4079])
* LimitOffset pagination crashes Browsable API when limit=0. ([#4079][gh4079])
* Fixed DecimalField arbitrary precision support. ([#4075][gh4075])
* Added support for custom CSRF cookie names. ([#4049][gh4049])
* Fix regression introduced by #4035. ([#4041][gh4041])

View File

@ -19,6 +19,10 @@ There are a wide range of resources available for learning and using Django REST
</a>
</div>
## Courses
* [Developing RESTful APIs with Django REST Framework][developing-restful-apis-with-django-rest-framework]
## Tutorials
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
@ -130,3 +134,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[pycon-us-2017]: https://www.youtube.com/watch?v=Rk6MHZdust4
[django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/
[doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/
[developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -48,7 +48,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
* The [Web browsable API][sandbox] is a huge usability win for your developers.
* The Web browsable API is a huge usability win for your developers.
* [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
@ -74,10 +74,11 @@ continued development by **[signing up for a paid plan][funding]**.
<li><a href="https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/135996800-d49fe024-32d9-441a-98d9-4c7596287a67.png)">PostHog</a></li>
<li><a href="https://cryptapi.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cryptapi.png)">CryptAPI</a></li>
<li><a href="https://www.fezto.xyz/?utm_source=DjangoRESTFramework" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/fezto.png)">FEZTO</a></li>
<li><a href="https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/svix.png)">Svix</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), and [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework).*
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), and [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship).*
---
@ -86,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
* Django (3.0, 3.1, 3.2, 4.0, 4.1)
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.

View File

@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response
[cite]: https://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/
[csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax
[csrf-ajax]: https://docs.djangoproject.com/en/stable/howto/csrf/#using-csrf-protection-with-ajax
[cors]: https://www.w3.org/TR/cors/
[adamchainz]: https://github.com/adamchainz
[django-cors-headers]: https://github.com/adamchainz/django-cors-headers

View File

@ -15,6 +15,18 @@ If you include fully-qualified URLs in your resource output, they will be 'urliz
By default, the API will return the format specified by the headers, which in the case of the browser is HTML. The format can be specified using `?format=` in the request, so you can look at the raw JSON response in a browser by adding `?format=json` to the URL. There are helpful extensions for viewing JSON in [Firefox][ffjsonview] and [Chrome][chromejsonview].
## Authentication
To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf:
```python
urlpatterns = [
# ...
url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
]
```
## Customizing
The browsable API is built with [Twitter's Bootstrap][bootstrap] (v 3.4.1), making it easy to customize the look-and-feel.

View File

@ -96,10 +96,14 @@ urlpatterns = [
# ...
# Route TemplateView to serve Swagger UI template.
# * Provide `extra_context` with view name of `SchemaView`.
path('swagger-ui/', TemplateView.as_view(
template_name='swagger-ui.html',
extra_context={'schema_url':'openapi-schema'}
), name='swagger-ui'),
path(
"swagger-ui/",
TemplateView.as_view(
template_name="swagger-ui.html",
extra_context={"schema_url": "openapi-schema"},
),
name="swagger-ui",
),
]
```
@ -145,10 +149,13 @@ urlpatterns = [
# ...
# Route TemplateView to serve the ReDoc template.
# * Provide `extra_context` with view name of `SchemaView`.
path('redoc/', TemplateView.as_view(
template_name='redoc.html',
extra_context={'schema_url':'openapi-schema'}
), name='redoc'),
path(
"redoc/",
TemplateView.as_view(
template_name="redoc.html", extra_context={"schema_url": "openapi-schema"}
),
name="redoc",
),
]
```

View File

@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
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. We can remove the two view classes, and replace them with a single ViewSet class:
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

View File

@ -105,7 +105,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
queryset = Group.objects.all().order_by('name')
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]

View File

@ -1,5 +1,5 @@
# MkDocs to build our documentation.
mkdocs>=1.1.2,<1.2
mkdocs==1.2.4
jinja2>=2.10,<3.1.0 # contextfilter has been renamed
# pylinkvalidator to check for broken links in documentation.

View File

@ -4,7 +4,7 @@ coreschema==0.0.4
django-filter
django-guardian>=2.4.0,<2.5
inflection==0.5.1
markdown==3.3
markdown>=3.3.7
psycopg2-binary>=2.9.5,<2.10
pygments==2.12
pygments>=2.12.0,<2.14.0
pyyaml>=5.3.1,<5.4

View File

@ -1,5 +1,5 @@
# Pytest for running the tests.
pytest>=6.2.0,<8.0
pytest>=7.0.1,<8.0
pytest-cov>=4.0.0,<5.0
pytest-django>=4.5.2,<5.0
importlib-metadata<5.0

View File

@ -169,6 +169,21 @@ else:
}
if django.VERSION >= (5, 1):
# Django 5.1+: use the stock ip_address_validators function
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
# 1) the list of validators and 2) the error message. Starting from
# Django 5.1 ip_address_validators only returns the list of validators
from django.core.validators import ip_address_validators
else:
# Django <= 5.1: create a compatibility shim for ip_address_validators
from django.core.validators import \
ip_address_validators as _ip_address_validators
def ip_address_validators(protocol, unpack_ipv4):
return _ip_address_validators(protocol, unpack_ipv4)[0]
# `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':')

View File

@ -36,7 +36,7 @@ def api_view(http_method_names=None):
# WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
# api_view applied without (method_names)
assert not(isinstance(http_method_names, types.FunctionType)), \
assert not isinstance(http_method_names, types.FunctionType), \
'@api_view missing list of allowed HTTP methods'
# api_view applied with eg. string instead of list of strings

View File

@ -16,7 +16,7 @@ from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import (
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, ProhibitNullCharactersValidator, RegexValidator,
URLValidator, ip_address_validators
URLValidator
)
from django.forms import FilePathField as DjangoFilePathField
from django.forms import ImageField as DjangoImageField
@ -36,6 +36,7 @@ except ImportError:
pytz = None
from rest_framework import ISO_8601
from rest_framework.compat import ip_address_validators
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, json, representation
@ -866,7 +867,7 @@ class IPAddressField(CharField):
self.protocol = protocol.lower()
self.unpack_ipv4 = (self.protocol == 'both')
super().__init__(**kwargs)
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
validators = ip_address_validators(protocol, self.unpack_ipv4)
self.validators.extend(validators)
def to_internal_value(self, data):

View File

@ -21,14 +21,14 @@ from rest_framework.settings import api_settings
def search_smart_split(search_terms):
"""generator that first splits string by spaces, leaving quoted phrases togheter,
"""generator that first splits string by spaces, leaving quoted phrases together,
then it splits non-quoted phrases by commas.
"""
for term in smart_split(search_terms):
# trim commas to avoid bad matching for quoted phrases
term = term.strip(',')
if term.startswith(('"', "'")) and term[0] == term[-1]:
# quoted phrases are kept togheter without any other split
# quoted phrases are kept together without any other split
yield unescape_string_literal(term)
else:
# non-quoted tokens are split by comma, keeping only non-empty ones

View File

@ -102,12 +102,12 @@ class EndpointEnumerator:
Given a URL conf regex, return a URI template string.
"""
# ???: Would it be feasible to adjust this such that we generate the
# path, plus the kwargs, plus the type from the convertor, such that we
# path, plus the kwargs, plus the type from the converter, such that we
# could feed that straight into the parameter schema object?
path = simplify_regex(path_regex)
# Strip Django 2.0 convertors as they are incompatible with uritemplate format
# Strip Django 2.0 converters as they are incompatible with uritemplate format
return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
def should_include_endpoint(self, path, callback):

View File

@ -84,7 +84,7 @@ class SchemaGenerator(BaseSchemaGenerator):
continue
if components_schemas[k] == components[k]:
continue
warnings.warn('Schema component "{}" has been overriden with a different value.'.format(k))
warnings.warn('Schema component "{}" has been overridden with a different value.'.format(k))
components_schemas.update(components)

View File

@ -116,7 +116,7 @@ DEFAULTS = {
'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True,
# Browseable API
# Browsable API
'HTML_SELECT_CUTOFF': 1000,
'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -250,7 +250,7 @@
"csrfToken": "{{ csrf_token }}"
}
</script>
<script src="{% static "rest_framework/js/jquery-3.6.4.min.js" %}"></script>
<script src="{% static "rest_framework/js/jquery-3.7.1.min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>

View File

@ -293,7 +293,7 @@
"csrfToken": "{% if request %}{{ csrf_token }}{% endif %}"
}
</script>
<script src="{% static "rest_framework/js/jquery-3.6.4.min.js" %}"></script>
<script src="{% static "rest_framework/js/jquery-3.7.1.min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>

View File

@ -66,6 +66,6 @@ at <code>rest_framework/docs/error.html</code>.</p>
<script src="{% static 'rest_framework/js/jquery-3.6.4.min.js' %}"></script>
<script src="{% static 'rest_framework/js/jquery-3.7.1.min.js' %}"></script>
</body>
</html>

View File

@ -38,7 +38,7 @@
{% include "rest_framework/docs/auth/basic.html" %}
{% include "rest_framework/docs/auth/session.html" %}
<script src="{% static 'rest_framework/js/jquery-3.6.4.min.js' %}"></script>
<script src="{% static 'rest_framework/js/jquery-3.7.1.min.js' %}"></script>
<script src="{% static 'rest_framework/js/bootstrap.min.js' %}"></script>
<script src="{% static 'rest_framework/docs/js/jquery.json-view.min.js' %}"></script>
<script src="{% static 'rest_framework/docs/js/api.js' %}"></script>

View File

@ -7,7 +7,7 @@ Usage: `get_field_info(model)` returns a `FieldInfo` instance.
"""
from collections import namedtuple
FieldInfo = namedtuple('FieldResult', [
FieldInfo = namedtuple('FieldInfo', [
'pk', # Model field instance
'fields', # Dict of field name -> model field instance
'forward_relations', # Dict of field name -> RelationInfo

View File

@ -160,10 +160,19 @@ class UniqueTogetherValidator:
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
# Ignore validation if any field is None
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
if None not in checked_values and qs_exists(queryset):
if serializer.instance is None:
checked_values = [
value for field, value in attrs.items() if field in self.fields
]
else:
# Ignore validation if all field values are unchanged
checked_values = [
value
for field, value in attrs.items()
if field in self.fields and value != getattr(serializer.instance, field)
]
if checked_values and None not in checked_values and qs_exists(queryset):
field_names = ', '.join(self.fields)
message = self.message.format(field_names=field_names)
raise ValidationError(message, code='unique')

View File

@ -1,4 +1,5 @@
#! /usr/bin/env python3
import os
import sys
import pytest
@ -34,6 +35,18 @@ if __name__ == "__main__":
'--cov-report', 'xml',
] + pytest_args
try:
pytest_args.remove('--no-pkgroot')
except ValueError:
pass
else:
sys.path.pop(0)
# import rest_framework before pytest re-adds the package root directory.
import rest_framework
package_dir = os.path.join(os.getcwd(), 'rest_framework')
assert not rest_framework.__file__.startswith(package_dir)
if first_arg.startswith('-'):
# `runtests.py [flags]`
pytest_args = ['tests'] + pytest_args

View File

@ -1,15 +1,10 @@
import os
import sys
import django
from django.core import management
def pytest_addoption(parser):
parser.addoption('--no-pkgroot', action='store_true', default=False,
help='Remove package root directory from sys.path, ensuring that '
'rest_framework is imported from the installed site-packages. '
'Used for testing the distribution.')
parser.addoption('--staticfiles', action='store_true', default=False,
help='Run tests with static files collection, using manifest '
'staticfiles storage. Used for testing the distribution.')
@ -87,19 +82,15 @@ def pytest_configure(config):
'guardian',
)
if config.getoption('--no-pkgroot'):
sys.path.pop(0)
# import rest_framework before pytest re-adds the package root directory.
import rest_framework
package_dir = os.path.join(os.getcwd(), 'rest_framework')
assert not rest_framework.__file__.startswith(package_dir)
# Manifest storage will raise an exception if static files are not present (ie, a packaging failure).
if config.getoption('--staticfiles'):
import rest_framework
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
settings.STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
if django.VERSION < (4, 2):
settings.STATICFILES_STORAGE = backend
else:
settings.STORAGES['staticfiles']['BACKEND'] = backend
django.setup()

View File

@ -8,7 +8,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.urls import path
from rest_framework.compat import uritemplate, yaml
from rest_framework.compat import coreapi, uritemplate, yaml
from rest_framework.management.commands import generateschema
from rest_framework.utils import formatting, json
from rest_framework.views import APIView
@ -91,6 +91,7 @@ class GenerateSchemaTests(TestCase):
os.remove(path)
@pytest.mark.skipif(yaml is None, reason='PyYAML is required.')
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_coreapi_renders_default_schema_with_custom_title_url_and_description(self):
expected_out = """info:
@ -113,6 +114,7 @@ class GenerateSchemaTests(TestCase):
self.assertIn(formatting.dedent(expected_out), self.out.getvalue())
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_coreapi_renders_openapi_json_schema(self):
expected_out = {
@ -142,6 +144,7 @@ class GenerateSchemaTests(TestCase):
self.assertDictEqual(out_json, expected_out)
@pytest.mark.skipif(coreapi is None, reason='coreapi is required.')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_renders_corejson_schema(self):
expected_out = """{"_type":"document","":{"list":{"_type":"link","url":"/","action":"get"}}}"""

View File

@ -1347,7 +1347,7 @@ class TestGenerator(TestCase):
assert len(w) == 1
assert issubclass(w[-1].category, UserWarning)
assert 'has been overriden with a different value.' in str(w[-1].message)
assert 'has been overridden with a different value.' in str(w[-1].message)
assert 'components' in schema
assert 'schemas' in schema['components']

View File

@ -132,7 +132,7 @@ urlpatterns = [
]
# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ...
# TODO: Clean tests below - remove duplicates with above, better unit testing, ...
@override_settings(ROOT_URLCONF='tests.test_response')
class RendererIntegrationTests(TestCase):
"""

View File

@ -223,7 +223,7 @@ class TestNotRequiredNestedSerializerWithMany:
input_data = {}
serializer = self.Serializer(data=input_data)
# request is empty, therefor 'nested' should not be in serializer.data
# request is empty, therefore 'nested' should not be in serializer.data
assert serializer.is_valid()
assert 'nested' not in serializer.validated_data
@ -237,7 +237,7 @@ class TestNotRequiredNestedSerializerWithMany:
input_data = QueryDict('')
serializer = self.Serializer(data=input_data)
# the querydict is empty, therefor 'nested' should not be in serializer.data
# the querydict is empty, therefore 'nested' should not be in serializer.data
assert serializer.is_valid()
assert 'nested' not in serializer.validated_data

View File

@ -192,7 +192,7 @@ class ThrottlingTests(TestCase):
if expect is not None:
assert response['Retry-After'] == expect
else:
assert not'Retry-After' in response
assert 'Retry-After' not in response
def test_seconds_fields(self):
"""

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.utils import json
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.utils.formatting import lazy_format
from rest_framework.utils.model_meta import FieldInfo, RelationInfo
from rest_framework.utils.urls import remove_query_param, replace_query_param
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
@ -267,3 +268,9 @@ class LazyFormatTests(TestCase):
assert message.format.call_count == 1
str(formatted)
assert message.format.call_count == 1
class ModelMetaNamedTupleNames(TestCase):
def test_named_tuple_names(self):
assert FieldInfo.__name__ == 'FieldInfo'
assert RelationInfo.__name__ == 'RelationInfo'

View File

@ -1,5 +1,5 @@
import datetime
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from django.db import DataError, models
@ -447,6 +447,22 @@ class TestUniquenessTogetherValidation(TestCase):
serializer = NullUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()
def test_ignore_validation_for_unchanged_fields(self):
"""
If all fields in the unique together constraint are unchanged,
then the instance should skip uniqueness validation.
"""
instance = UniquenessTogetherModel.objects.create(
race_name="Paris Marathon", position=1
)
data = {"race_name": "Paris Marathon", "position": 1}
serializer = UniquenessTogetherSerializer(data=data, instance=instance)
with patch(
"rest_framework.validators.qs_exists"
) as mock:
assert serializer.is_valid()
assert not mock.called
def test_filter_queryset_do_not_skip_existing_attribute(self):
"""
filter_queryset should add value from existing instance attribute