Merge branch 'master' into style-changes

This commit is contained in:
Francisco Couzo 2022-10-17 16:39:26 -03:00 committed by GitHub
commit bb96232b54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 517 additions and 465 deletions

View File

@ -1,7 +1,7 @@
include README.md include README.md
include LICENSE.md include LICENSE.md
recursive-include tests/ * recursive-include tests/ *
recursive-include rest_framework/static *.js *.css *.png *.ico *.eot *.svg *.ttf *.woff *.woff2 recursive-include rest_framework/static *.js *.css *.map *.png *.ico *.eot *.svg *.ttf *.woff *.woff2
recursive-include rest_framework/templates *.html schema.js recursive-include rest_framework/templates *.html schema.js
recursive-include rest_framework/locale *.mo recursive-include rest_framework/locale *.mo
global-exclude __pycache__ global-exclude __pycache__

View File

@ -54,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (3.6, 3.7, 3.8, 3.9, 3.10) * Python 3.6+
* Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1) * Django 4.1, 4.0, 3.2, 3.1, 3.0
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
each Python and Django series. each Python and Django series.
@ -90,9 +90,10 @@ Startup up a new project like so...
Now edit the `example/urls.py` module in your project: Now edit the `example/urls.py` module in your project:
```python ```python
from django.urls import path, include
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers, viewsets, routers from django.urls import include, path
from rest_framework import routers, serializers, viewsets
# Serializers define the API representation. # Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
@ -111,7 +112,6 @@ class UserViewSet(viewsets.ModelViewSet):
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', UserViewSet) router.register(r'users', UserViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
@ -185,7 +185,7 @@ Please see the [security policy][security-policy].
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master [codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg [pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
[pypi]: https://pypi.org/project/djangorestframework/ [pypi]: https://pypi.org/project/djangorestframework/
[twitter]: https://twitter.com/_tomchristie [twitter]: https://twitter.com/starletdreaming
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[sandbox]: https://restframework.herokuapp.com/ [sandbox]: https://restframework.herokuapp.com/

View File

@ -173,9 +173,9 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
--- ---
#### Generating Tokens ### Generating Tokens
##### By using signals #### By using signals
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
@ -199,7 +199,7 @@ If you've already created some users, you can generate tokens for all existing u
for user in User.objects.all(): for user in User.objects.all():
Token.objects.get_or_create(user=user) Token.objects.get_or_create(user=user)
##### By exposing an api endpoint #### By exposing an api endpoint
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf: When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
@ -248,7 +248,7 @@ And in your `urls.py`:
] ]
##### With Django admin #### With Django admin
It is also possible to create Tokens manually through the admin interface. In case you are using a large user base, we recommend that you monkey patch the `TokenAdmin` class to customize it to your needs, more specifically by declaring the `user` field as `raw_field`. It is also possible to create Tokens manually through the admin interface. In case you are using a large user base, we recommend that you monkey patch the `TokenAdmin` class to customize it to your needs, more specifically by declaring the `user` field as `raw_field`.
@ -369,7 +369,7 @@ The following third-party packages are also available.
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
#### Installation & configuration ### Installation & configuration
Install using `pip`. Install using `pip`.
@ -396,7 +396,7 @@ The [Django REST framework OAuth][django-rest-framework-oauth] package provides
This package was previously included directly in the REST framework but is now supported and maintained as a third-party package. This package was previously included directly in the REST framework but is now supported and maintained as a third-party package.
#### Installation & configuration ### Installation & configuration
Install the package using `pip`. Install the package using `pip`.

View File

@ -78,7 +78,7 @@ Defaults to `False`
### `source` ### `source`
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`. The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
When serializing fields with dotted notation, it may be necessary to provide a `default` value if any object is not present or is empty during attribute traversal. Beware of possible n+1 problems when using source attribute if you are accessing a relational orm model. For example: When serializing fields with dotted notation, it may be necessary to provide a `default` value if any object is not present or is empty during attribute traversal. Beware of possible n+1 problems when using source attribute if you are accessing a relational orm model. For example:
@ -159,14 +159,6 @@ Corresponds to `django.db.models.fields.BooleanField`.
**Signature:** `BooleanField()` **Signature:** `BooleanField()`
## NullBooleanField
A boolean representation that also accepts `None` as a valid value.
Corresponds to `django.db.models.fields.NullBooleanField`.
**Signature:** `NullBooleanField()`
--- ---
# String fields # String fields
@ -179,10 +171,10 @@ Corresponds to `django.db.models.fields.CharField` or `django.db.models.fields.T
**Signature:** `CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)` **Signature:** `CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)`
- `max_length` - Validates that the input contains no more than this number of characters. * `max_length` - Validates that the input contains no more than this number of characters.
- `min_length` - Validates that the input contains no fewer than this number of characters. * `min_length` - Validates that the input contains no fewer than this number of characters.
- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. * `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
- `trim_whitespace` - If set to `True` then leading and trailing whitespace is trimmed. Defaults to `True`. * `trim_whitespace` - If set to `True` then leading and trailing whitespace is trimmed. Defaults to `True`.
The `allow_null` option is also available for string fields, although its usage is discouraged in favor of `allow_blank`. It is valid to set both `allow_blank=True` and `allow_null=True`, but doing so means that there will be two differing types of empty value permissible for string representations, which can lead to data inconsistencies and subtle application bugs. The `allow_null` option is also available for string fields, although its usage is discouraged in favor of `allow_blank`. It is valid to set both `allow_blank=True` and `allow_null=True`, but doing so means that there will be two differing types of empty value permissible for string representations, which can lead to data inconsistencies and subtle application bugs.
@ -230,11 +222,11 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m
**Signature:** `UUIDField(format='hex_verbose')` **Signature:** `UUIDField(format='hex_verbose')`
- `format`: Determines the representation format of the uuid value * `format`: Determines the representation format of the uuid value
- `'hex_verbose'` - The canonical hex representation, including hyphens: `"5ce0e9a5-5ffa-654b-cee0-1238041fb31a"` * `'hex_verbose'` - The canonical hex representation, including hyphens: `"5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
- `'hex'` - The compact hex representation of the UUID, not including hyphens: `"5ce0e9a55ffa654bcee01238041fb31a"` * `'hex'` - The compact hex representation of the UUID, not including hyphens: `"5ce0e9a55ffa654bcee01238041fb31a"`
- `'int'` - A 128 bit integer representation of the UUID: `"123456789012312313134124512351145145114"` * `'int'` - A 128 bit integer representation of the UUID: `"123456789012312313134124512351145145114"`
- `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"` * `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value` Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value`
## FilePathField ## FilePathField
@ -245,11 +237,11 @@ Corresponds to `django.forms.fields.FilePathField`.
**Signature:** `FilePathField(path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs)` **Signature:** `FilePathField(path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs)`
- `path` - The absolute filesystem path to a directory from which this FilePathField should get its choice. * `path` - The absolute filesystem path to a directory from which this FilePathField should get its choice.
- `match` - A regular expression, as a string, that FilePathField will use to filter filenames. * `match` - A regular expression, as a string, that FilePathField will use to filter filenames.
- `recursive` - Specifies whether all subdirectories of path should be included. Default is `False`. * `recursive` - Specifies whether all subdirectories of path should be included. Default is `False`.
- `allow_files` - Specifies whether files in the specified location should be included. Default is `True`. Either this or `allow_folders` must be `True`. * `allow_files` - Specifies whether files in the specified location should be included. Default is `True`. Either this or `allow_folders` must be `True`.
- `allow_folders` - Specifies whether folders in the specified location should be included. Default is `False`. Either this or `allow_files` must be `True`. * `allow_folders` - Specifies whether folders in the specified location should be included. Default is `False`. Either this or `allow_files` must be `True`.
## IPAddressField ## IPAddressField
@ -259,8 +251,8 @@ Corresponds to `django.forms.fields.IPAddressField` and `django.forms.fields.Gen
**Signature**: `IPAddressField(protocol='both', unpack_ipv4=False, **options)` **Signature**: `IPAddressField(protocol='both', unpack_ipv4=False, **options)`
- `protocol` Limits valid inputs to the specified protocol. Accepted values are 'both' (default), 'IPv4' or 'IPv6'. Matching is case insensitive. * `protocol` Limits valid inputs to the specified protocol. Accepted values are 'both' (default), 'IPv4' or 'IPv6'. Matching is case insensitive.
- `unpack_ipv4` Unpacks IPv4 mapped addresses like ::ffff:192.0.2.1. If this option is enabled that address would be unpacked to 192.0.2.1. Default is disabled. Can only be used when protocol is set to 'both'. * `unpack_ipv4` Unpacks IPv4 mapped addresses like ::ffff:192.0.2.1. If this option is enabled that address would be unpacked to 192.0.2.1. Default is disabled. Can only be used when protocol is set to 'both'.
--- ---
@ -274,8 +266,8 @@ Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.
**Signature**: `IntegerField(max_value=None, min_value=None)` **Signature**: `IntegerField(max_value=None, min_value=None)`
- `max_value` Validate that the number provided is no greater than this value. * `max_value` Validate that the number provided is no greater than this value.
- `min_value` Validate that the number provided is no less than this value. * `min_value` Validate that the number provided is no less than this value.
## FloatField ## FloatField
@ -285,8 +277,8 @@ Corresponds to `django.db.models.fields.FloatField`.
**Signature**: `FloatField(max_value=None, min_value=None)` **Signature**: `FloatField(max_value=None, min_value=None)`
- `max_value` Validate that the number provided is no greater than this value. * `max_value` Validate that the number provided is no greater than this value.
- `min_value` Validate that the number provided is no less than this value. * `min_value` Validate that the number provided is no less than this value.
## DecimalField ## DecimalField
@ -296,13 +288,13 @@ Corresponds to `django.db.models.fields.DecimalField`.
**Signature**: `DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)` **Signature**: `DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)`
- `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`. * `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`.
- `decimal_places` The number of decimal places to store with the number. * `decimal_places` The number of decimal places to store with the number.
- `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`. * `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`.
- `max_value` Validate that the number provided is no greater than this value. * `max_value` Validate that the number provided is no greater than this value.
- `min_value` Validate that the number provided is no less than this value. * `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. * `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 quantising to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`. * `rounding` Sets the rounding mode used when quantising to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
#### Example usage #### Example usage
@ -314,10 +306,6 @@ And to validate numbers up to anything less than one billion with a resolution o
serializers.DecimalField(max_digits=19, decimal_places=10) serializers.DecimalField(max_digits=19, decimal_places=10)
This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
--- ---
# Date and time fields # Date and time fields
@ -392,8 +380,8 @@ The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu
**Signature:** `DurationField(max_value=None, min_value=None)` **Signature:** `DurationField(max_value=None, min_value=None)`
- `max_value` Validate that the duration provided is no greater than this value. * `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. * `min_value` Validate that the duration provided is no less than this value.
--- ---
@ -407,10 +395,10 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding
**Signature:** `ChoiceField(choices)` **Signature:** `ChoiceField(choices)`
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples. * `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. * `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`. * `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`.
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` * `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices. Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
@ -420,10 +408,10 @@ A field that can accept a set of zero, one or many values, chosen from a limited
**Signature:** `MultipleChoiceField(choices)` **Signature:** `MultipleChoiceField(choices)`
- `choices` - A list of valid values, or a list of `(key, display_name)` tuples. * `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. * `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`. * `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`.
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` * `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices. As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
@ -444,9 +432,9 @@ Corresponds to `django.forms.fields.FileField`.
**Signature:** `FileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)` **Signature:** `FileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)`
- `max_length` - Designates the maximum length for the file name. * `max_length` - Designates the maximum length for the file name.
- `allow_empty_file` - Designates if empty files are allowed. * `allow_empty_file` - Designates if empty files are allowed.
- `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise. * `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise.
## ImageField ## ImageField
@ -456,9 +444,9 @@ Corresponds to `django.forms.fields.ImageField`.
**Signature:** `ImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)` **Signature:** `ImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)`
- `max_length` - Designates the maximum length for the file name. * `max_length` - Designates the maximum length for the file name.
- `allow_empty_file` - Designates if empty files are allowed. * `allow_empty_file` - Designates if empty files are allowed.
- `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise. * `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise.
Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained. Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained.
@ -472,10 +460,10 @@ A field class that validates a list of objects.
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)` **Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`
- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated. * `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
- `allow_empty` - Designates if empty lists are allowed. * `allow_empty` - Designates if empty lists are allowed.
- `min_length` - Validates that the list contains no fewer than this number of elements. * `min_length` - Validates that the list contains no fewer than this number of elements.
- `max_length` - Validates that the list contains no more than this number of elements. * `max_length` - Validates that the list contains no more than this number of elements.
For example, to validate a list of integers you might use something like the following: For example, to validate a list of integers you might use something like the following:
@ -496,8 +484,8 @@ A field class that validates a dictionary of objects. The keys in `DictField` ar
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)` **Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated. * `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
- `allow_empty` - Designates if empty dictionaries are allowed. * `allow_empty` - Designates if empty dictionaries are allowed.
For example, to create a field that validates a mapping of strings to strings, you would write something like this: For example, to create a field that validates a mapping of strings to strings, you would write something like this:
@ -514,8 +502,8 @@ A preconfigured `DictField` that is compatible with Django's postgres `HStoreFie
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>, allow_empty=True)` **Signature**: `HStoreField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
- `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values. * `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values.
- `allow_empty` - Designates if empty dictionaries are allowed. * `allow_empty` - Designates if empty dictionaries are allowed.
Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings. Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.
@ -525,8 +513,8 @@ A field class that validates that the incoming data structure consists of valid
**Signature**: `JSONField(binary, encoder)` **Signature**: `JSONField(binary, encoder)`
- `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather than a primitive data structure. Defaults to `False`. * `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather than a primitive data structure. Defaults to `False`.
- `encoder` - Use this JSON encoder to serialize input object. Defaults to `None`. * `encoder` - Use this JSON encoder to serialize input object. Defaults to `None`.
--- ---
@ -577,7 +565,7 @@ This is a read-only field. It gets its value by calling a method on the serializ
**Signature**: `SerializerMethodField(method_name=None)` **Signature**: `SerializerMethodField(method_name=None)`
- `method_name` - The name of the method on the serializer to be called. If not included this defaults to `get_<field_name>`. * `method_name` - The name of the method on the serializer to be called. If not included this defaults to `get_<field_name>`.
The serializer method referred to by the `method_name` argument should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: The serializer method referred to by the `method_name` argument should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:

View File

@ -65,7 +65,7 @@ The following attributes control the basic view behavior.
* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. If you are overriding a view method, it is important that you call `get_queryset()` instead of accessing this property directly, as `queryset` will get evaluated once, and those results will be cached for all subsequent requests. * `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. If you are overriding a view method, it is important that you call `get_queryset()` instead of accessing this property directly, as `queryset` will get evaluated once, and those results will be cached for all subsequent requests.
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method. * `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value. * `lookup_field` - The model field that should be used for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value.
* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`. * `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`.
**Pagination**: **Pagination**:
@ -217,7 +217,7 @@ If the request data provided for creating the object was invalid, a `400 Bad Req
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`. If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise, it will return a `404 Not Found`.
## UpdateModelMixin ## UpdateModelMixin
@ -335,7 +335,7 @@ For example, if you need to lookup objects based on multiple fields in the URL c
queryset = self.filter_queryset(queryset) # Apply any filter backends queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {} filter = {}
for field in self.lookup_fields: for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields. if self.kwargs.get(field): # Ignore empty fields.
filter[field] = self.kwargs[field] filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj) self.check_object_permissions(self.request, obj)
@ -395,4 +395,4 @@ The following third party packages provide additional generic view implementatio
[UpdateModelMixin]: #updatemodelmixin [UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin [DestroyModelMixin]: #destroymodelmixin
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [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/3.1/ref/models/querysets/#django.db.models.query.QuerySet.select_related

View File

@ -171,7 +171,7 @@ This permission is suitable if you want to your API to allow read permissions to
## DjangoModelPermissions ## DjangoModelPermissions
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. The appropriate model is determined by checking `get_queryset().model` or `queryset.model`.
* `POST` requests require the user to have the `add` permission on the model. * `POST` requests require the user to have the `add` permission on the model.
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model.

View File

@ -33,7 +33,7 @@ For example, the following serializer would lead to a database hit each time eva
class Meta: class Meta:
model = Album model = Album
fields = ['album_name', 'artist', 'tracks'] fields = ['album_name', 'artist', 'tracks']
# For each album object, tracks should be fetched from database # For each album object, tracks should be fetched from database
qs = Album.objects.all() qs = Album.objects.all()
print(AlbumSerializer(qs, many=True).data) print(AlbumSerializer(qs, many=True).data)
@ -278,7 +278,7 @@ This field is always read-only.
As opposed to previously discussed _references_ to another entity, the referred entity can instead also be embedded or _nested_ As opposed to previously discussed _references_ to another entity, the referred entity can instead also be embedded or _nested_
in the representation of the object that refers to it. in the representation of the object that refers to it.
Such nested relationships can be expressed by using serializers as fields. Such nested relationships can be expressed by using serializers as fields.
If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field.
@ -494,8 +494,8 @@ This behavior is intended to prevent a template from being unable to render in a
There are two keyword arguments you can use to control this behavior: There are two keyword arguments you can use to control this behavior:
- `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Set to `None` to disable any limiting. Defaults to `1000`. * `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Set to `None` to disable any limiting. Defaults to `1000`.
- `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` * `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"`
You can also control these globally using the settings `HTML_SELECT_CUTOFF` and `HTML_SELECT_CUTOFF_TEXT`. You can also control these globally using the settings `HTML_SELECT_CUTOFF` and `HTML_SELECT_CUTOFF_TEXT`.

View File

@ -122,6 +122,7 @@ The `get_schema_view()` helper takes the following keyword arguments:
url='https://www.example.org/api/', url='https://www.example.org/api/',
patterns=schema_url_patterns, patterns=schema_url_patterns,
) )
* `public`: May be used to specify if schema should bypass views permissions. Default to False
* `generator_class`: May be used to specify a `SchemaGenerator` subclass to be * `generator_class`: May be used to specify a `SchemaGenerator` subclass to be
passed to the `SchemaView`. passed to the `SchemaView`.

View File

@ -602,7 +602,7 @@ A mapping of Django model fields to REST framework serializer fields. You can ov
This property should be the serializer field class, that is used for relational fields by default. This property should be the serializer field class, that is used for relational fields by default.
For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `ModelSerializer` this defaults to `serializers.PrimaryKeyRelatedField`.
For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`. For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`.
@ -886,7 +886,7 @@ Because this class provides the same interface as the `Serializer` class, you ca
The only difference you'll notice when doing so is the `BaseSerializer` classes will not generate HTML forms in the browsable API. This is because the data they return does not include all the field information that would allow each field to be rendered into a suitable HTML input. The only difference you'll notice when doing so is the `BaseSerializer` classes will not generate HTML forms in the browsable API. This is because the data they return does not include all the field information that would allow each field to be rendered into a suitable HTML input.
##### Read-only `BaseSerializer` classes #### Read-only `BaseSerializer` classes
To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model: To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model:
@ -920,7 +920,7 @@ Or use it to serialize multiple instances:
serializer = HighScoreSerializer(queryset, many=True) serializer = HighScoreSerializer(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
##### Read-write `BaseSerializer` classes #### Read-write `BaseSerializer` classes
To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `serializers.ValidationError` if the supplied data is in an incorrect format. To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `serializers.ValidationError` if the supplied data is in an incorrect format.
@ -969,7 +969,7 @@ Here's a complete example of our previous `HighScoreSerializer`, that's been upd
The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles, or for integrating with alternative storage backends. The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles, or for integrating with alternative storage backends.
The following class is an example of a generic serializer that can handle coercing arbitrary objects into primitive representations. The following class is an example of a generic serializer that can handle coercing arbitrary complex objects into primitive representations.
class ObjectSerializer(serializers.BaseSerializer): class ObjectSerializer(serializers.BaseSerializer):
""" """
@ -1189,7 +1189,7 @@ The [drf-writable-nested][drf-writable-nested] package provides writable nested
## DRF Encrypt Content ## DRF Encrypt Content
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. 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.
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion

View File

@ -0,0 +1,62 @@
<style>
.promo li a {
float: left;
width: 130px;
height: 20px;
text-align: center;
margin: 10px 30px;
padding: 150px 0 0 0;
background-position: 0 50%;
background-size: 130px auto;
background-repeat: no-repeat;
font-size: 120%;
color: black;
}
.promo li {
list-style: none;
}
</style>
# Django REST framework 3.14
## Django 4.1 support
The latest release now fully supports Django 4.1, and drops support for Django 2.2.
Our requirements are now:
* Python 3.6+
* Django 4.1, 4.0, 3.2, 3.1, 3.0
## `raise_exceptions` argument for `is_valid` is now keyword-only.
Calling `serializer_instance.is_valid(True)` is no longer acceptable syntax.
If you'd like to use the `raise_exceptions` argument, you must use it as a
keyword argument.
See Pull Request [#7952](https://github.com/encode/django-rest-framework/pull/7952) for more details.
## `ManyRelatedField` supports returning the default when the source attribute doesn't exist.
Previously, if you used a serializer field with `many=True` with a dot notated source field
that didn't exist, it would raise an `AttributeError`. Now it will return the default or be
skipped depending on the other arguments.
See Pull Request [#7574](https://github.com/encode/django-rest-framework/pull/7574) for more details.
## Make Open API `get_reference` public.
Returns a reference to the serializer component. This may be useful if you override `get_schema()`.
## Change semantic of OR of two permission classes.
When OR-ing two permissions, the request has to pass either class's `has_permission() and has_object_permission()`.
Previously, both class's `has_permission()` was ignored when OR-ing two permissions together.
See Pull Request [#7522](https://github.com/encode/django-rest-framework/pull/7522) for more details.
## Minor fixes and improvements
There are a number of minor fixes and improvements in this release. See the [release notes](release-notes.md) page for a complete listing.

View File

@ -80,7 +80,7 @@ To run the tests, clone the repository, and then:
# Setup the virtual environment # Setup the virtual environment
python3 -m venv env python3 -m venv env
source env/bin/activate source env/bin/activate
pip install django pip install -e .
pip install -r requirements.txt pip install -r requirements.txt
# Run the tests # Run the tests

View File

@ -137,7 +137,7 @@ REST framework continues to be open-source and permissively licensed, but we fir
## What future funding will enable ## What future funding will enable
* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries. * Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries.
* Better authentication defaults, possibly bringing JWT & CORs support into the core package. * Better authentication defaults, possibly bringing JWT & CORS support into the core package.
* Securing the community & operations manager position long-term. * Securing the community & operations manager position long-term.
* Opening up and securing a part-time position to focus on ticket triage and resolution. * Opening up and securing a part-time position to focus on ticket triage and resolution.
* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation. * Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation.

View File

@ -11,7 +11,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
* [https://djangojobs.net/jobs/][django-jobs-net] * [https://djangojobs.net/jobs/][django-jobs-net]
* [https://findwork.dev/django-rest-framework-jobs][findwork-dev] * [https://findwork.dev/django-rest-framework-jobs][findwork-dev]
* [https://www.indeed.com/q-Django-jobs.html][indeed-com] * [https://www.indeed.com/q-Django-jobs.html][indeed-com]
* [https://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-com] * [https://stackoverflow.com/jobs/companies?tl=django][stackoverflow-com]
* [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com] * [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com]
* [https://www.technojobs.co.uk/django-jobs][technobjobs-co-uk] * [https://www.technojobs.co.uk/django-jobs][technobjobs-co-uk]
* [https://remoteok.io/remote-django-jobs][remoteok-io] * [https://remoteok.io/remote-django-jobs][remoteok-io]
@ -29,7 +29,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
[django-jobs-net]: https://djangojobs.net/jobs/ [django-jobs-net]: https://djangojobs.net/jobs/
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs [findwork-dev]: https://findwork.dev/django-rest-framework-jobs
[indeed-com]: https://www.indeed.com/q-Django-jobs.html [indeed-com]: https://www.indeed.com/q-Django-jobs.html
[stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django [stackoverflow-com]: https://stackoverflow.com/jobs/companies?tl=django
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/ [upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/
[technobjobs-co-uk]: https://www.technojobs.co.uk/django-jobs [technobjobs-co-uk]: https://www.technojobs.co.uk/django-jobs
[remoteok-io]: https://remoteok.io/remote-django-jobs [remoteok-io]: https://remoteok.io/remote-django-jobs

View File

@ -34,6 +34,25 @@ You can determine your currently installed version using `pip show`:
--- ---
## 3.14.x series
### 3.14.0
Date: 22nd September 2022
* Django 2.2 is no longer supported. [[#8662](https://github.com/encode/django-rest-framework/pull/8662)]
* Django 4.1 compatibility. [[#8591](https://github.com/encode/django-rest-framework/pull/8591)]
* Add `--api-version` CLI option to `generateschema` management command. [[#8663](https://github.com/encode/django-rest-framework/pull/8663)]
* Enforce `is_valid(raise_exception=False)` as a keyword-only argument. [[#7952](https://github.com/encode/django-rest-framework/pull/7952)]
* 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)]
* 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)]
* Update throttling to check if `request.user` is set before checking if the user is authenticated. [[#8370](https://github.com/encode/django-rest-framework/pull/8370)]
## 3.13.x series ## 3.13.x series
### 3.13.1 ### 3.13.1

View File

@ -17,7 +17,7 @@ By default, the API will return the format specified by the headers, which in th
## Customizing ## Customizing
The browsable API is built with [Twitter's Bootstrap][bootstrap] (v 3.3.5), making it easy to customize the look-and-feel. The browsable API is built with [Twitter's Bootstrap][bootstrap] (v 3.4.1), making it easy to customize the look-and-feel.
To customize the default style, create a template called `rest_framework/api.html` that extends from `rest_framework/base.html`. For example: To customize the default style, create a template called `rest_framework/api.html` that extends from `rest_framework/base.html`. For example:
@ -35,7 +35,7 @@ To replace the default theme, add a `bootstrap_theme` block to your `api.html` a
<link rel="stylesheet" href="/path/to/my/bootstrap.css" type="text/css"> <link rel="stylesheet" href="/path/to/my/bootstrap.css" type="text/css">
{% endblock %} {% endblock %}
Suitable pre-made replacement themes are available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above. Suitable pre-made replacement themes are available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above. Make sure that the Bootstrap version of the new theme matches that of the default theme.
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style. You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
@ -44,7 +44,7 @@ Full example:
{% extends "rest_framework/base.html" %} {% extends "rest_framework/base.html" %}
{% block bootstrap_theme %} {% block bootstrap_theme %}
<link rel="stylesheet" href="https://bootswatch.com/flatly/bootstrap.min.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@3.4.1/flatly/bootstrap.min.css" type="text/css">
{% endblock %} {% endblock %}
{% block bootstrap_navbar_variant %}{% endblock %} {% block bootstrap_navbar_variant %}{% endblock %}

View File

@ -112,7 +112,7 @@ Now update the `snippets/urls.py` file slightly, to append a set of `format_suff
urlpatterns = [ urlpatterns = [
path('snippets/', views.snippet_list), path('snippets/', views.snippet_list),
path('snippets/<int:pk>', views.snippet_detail), path('snippets/<int:pk>/', views.snippet_detail),
] ]
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -112,8 +112,8 @@ Here's our re-wired `snippets/urls.py` file.
# Create a router and register our viewsets with it. # Create a router and register our viewsets with it.
router = DefaultRouter() router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, basename="snippet") router.register(r'snippets', views.SnippetViewSet, basename='snippet')
router.register(r'users', views.UserViewSet, basename="user") router.register(r'users', views.UserViewSet, basename='user')
# The API URLs are now determined automatically by the router. # The API URLs are now determined automatically by the router.
urlpatterns = [ urlpatterns = [

View File

@ -66,6 +66,7 @@ nav:
- 'Contributing to REST framework': 'community/contributing.md' - 'Contributing to REST framework': 'community/contributing.md'
- 'Project management': 'community/project-management.md' - 'Project management': 'community/project-management.md'
- 'Release Notes': 'community/release-notes.md' - 'Release Notes': 'community/release-notes.md'
- '3.14 Announcement': 'community/3.14-announcement.md'
- '3.13 Announcement': 'community/3.13-announcement.md' - '3.13 Announcement': 'community/3.13-announcement.md'
- '3.12 Announcement': 'community/3.12-announcement.md' - '3.12 Announcement': 'community/3.12-announcement.md'
- '3.11 Announcement': 'community/3.11-announcement.md' - '3.11 Announcement': 'community/3.11-announcement.md'

View File

@ -1,6 +1,7 @@
# The base set of requirements for REST framework is actually # The base set of requirements for REST framework is actually
# just Django, but for the purposes of development and testing # just Django and pytz, but for the purposes of development
# there are a number of packages that are useful to install. # and testing there are a number of packages that are useful
# to install.
# Laying these out as separate requirements files, allows us to # Laying these out as separate requirements files, allows us to
# only included the relevant sets when running tox, and ensures # only included the relevant sets when running tox, and ensures

View File

@ -3,8 +3,7 @@ coreapi==2.3.1
coreschema==0.0.4 coreschema==0.0.4
django-filter>=2.4.0,<3.0 django-filter>=2.4.0,<3.0
django-guardian>=2.4.0,<2.5 django-guardian>=2.4.0,<2.5
markdown==3.3;python_version>="3.6" markdown==3.3
markdown==3.2.2;python_version=="3.5"
psycopg2-binary>=2.8.5,<2.9 psycopg2-binary>=2.8.5,<2.9
pygments>=2.7.1,<2.8 pygments==2.12
pyyaml>=5.3.1,<5.4 pyyaml>=5.3.1,<5.4

View File

@ -2,3 +2,4 @@
pytest>=6.1,<7.0 pytest>=6.1,<7.0
pytest-cov>=2.10.1,<3.0 pytest-cov>=2.10.1,<3.0
pytest-django>=4.1.0,<5.0 pytest-django>=4.1.0,<5.0
importlib-metadata<5.0

View File

@ -10,7 +10,7 @@ ______ _____ _____ _____ __
import django import django
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.13.1' __version__ = '3.14.0'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 3-Clause' __license__ = 'BSD 3-Clause'
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
@ -29,9 +29,5 @@ if django.VERSION < (3, 2):
default_app_config = 'rest_framework.apps.RestFrameworkConfig' default_app_config = 'rest_framework.apps.RestFrameworkConfig'
class RemovedInDRF313Warning(DeprecationWarning): class RemovedInDRF315Warning(DeprecationWarning):
pass
class RemovedInDRF314Warning(PendingDeprecationWarning):
pass pass

View File

@ -2,6 +2,7 @@
The `compat` module provides support for backwards compatibility with older The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages. versions of Django/Python, and compatibility wrappers around optional packages.
""" """
import django
from django.conf import settings from django.conf import settings
from django.views.generic import View from django.views.generic import View
@ -152,6 +153,30 @@ else:
return False return False
if django.VERSION >= (4, 2):
# Django 4.2+: use the stock parse_header_parameters function
# Note: Django 4.1 also has an implementation of parse_header_parameters
# which is slightly different from the one in 4.2, it needs
# the compatibility shim as well.
from django.utils.http import parse_header_parameters
else:
# Django <= 4.1: create a compatibility shim for parse_header_parameters
from django.http.multipartparser import parse_header
def parse_header_parameters(line):
# parse_header works with bytes, but parse_header_parameters
# works with strings. Call encode to convert the line to bytes.
main_value_pair, params = parse_header(line.encode())
return main_value_pair, {
# parse_header will convert *some* values to string.
# parse_header_parameters converts *all* values to string.
# Make sure all values are converted by calling decode on
# any remaining non-string values.
k: v if isinstance(v, str) else v.decode()
for k, v in params.items()
}
# `separators` argument to `json.dumps()` differs between 2.x and 3.x # `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767 # See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':') SHORT_SEPARATORS = (',', ':')

View File

@ -1,7 +1,7 @@
""" """
Handled exceptions raised by REST framework. Handled exceptions raised by REST framework.
In addition Django's built in 403 and 404 exceptions are handled. In addition, Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) (`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
""" """
import math import math
@ -72,16 +72,19 @@ class ErrorDetail(str):
return self return self
def __eq__(self, other): def __eq__(self, other):
r = super().__eq__(other) result = super().__eq__(other)
if r is NotImplemented: if result is NotImplemented:
return NotImplemented return NotImplemented
try: try:
return r and self.code == other.code return result and self.code == other.code
except AttributeError: except AttributeError:
return r return result
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) result = self.__eq__(other)
if result is NotImplemented:
return NotImplemented
return not result
def __repr__(self): def __repr__(self):
return 'ErrorDetail(string=%r, code=%r)' % ( return 'ErrorDetail(string=%r, code=%r)' % (

View File

@ -1,3 +1,4 @@
import contextlib
import copy import copy
import datetime import datetime
import decimal import decimal
@ -5,7 +6,6 @@ import functools
import inspect import inspect
import re import re
import uuid import uuid
import warnings
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Mapping from collections.abc import Mapping
@ -30,9 +30,7 @@ from django.utils.ipv6 import clean_ipv6_address
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pytz.exceptions import InvalidTimeError from pytz.exceptions import InvalidTimeError
from rest_framework import ( from rest_framework import ISO_8601
ISO_8601, RemovedInDRF313Warning, RemovedInDRF314Warning
)
from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, json, representation from rest_framework.utils import html, humanize_datetime, json, representation
@ -265,16 +263,6 @@ class CreateOnlyDefault:
if is_update: if is_update:
raise SkipField() raise SkipField()
if callable(self.default): if callable(self.default):
if hasattr(self.default, 'set_context'):
warnings.warn(
"Method `set_context` on defaults is deprecated and will "
"no longer be called starting with 3.13. Instead set "
"`requires_context = True` on the class, and accept the "
"context as an additional argument.",
RemovedInDRF313Warning, stacklevel=2
)
self.default.set_context(self)
if getattr(self.default, 'requires_context', False): if getattr(self.default, 'requires_context', False):
return self.default(serializer_field) return self.default(serializer_field)
else: else:
@ -504,16 +492,6 @@ class Field:
# No default, or this is a partial update. # No default, or this is a partial update.
raise SkipField() raise SkipField()
if callable(self.default): if callable(self.default):
if hasattr(self.default, 'set_context'):
warnings.warn(
"Method `set_context` on defaults is deprecated and will "
"no longer be called starting with 3.13. Instead set "
"`requires_context = True` on the class, and accept the "
"context as an additional argument.",
RemovedInDRF313Warning, stacklevel=2
)
self.default.set_context(self)
if getattr(self.default, 'requires_context', False): if getattr(self.default, 'requires_context', False):
return self.default(self) return self.default(self)
else: else:
@ -578,16 +556,6 @@ class Field:
""" """
errors = [] errors = []
for validator in self.validators: for validator in self.validators:
if hasattr(validator, 'set_context'):
warnings.warn(
"Method `set_context` on validators is deprecated and will "
"no longer be called starting with 3.13. Instead set "
"`requires_context = True` on the class, and accept the "
"context as an additional argument.",
RemovedInDRF313Warning, stacklevel=2
)
validator.set_context(self)
try: try:
if getattr(validator, 'requires_context', False): if getattr(validator, 'requires_context', False):
validator(value, self) validator(value, self)
@ -723,15 +691,13 @@ class BooleanField(Field):
NULL_VALUES = {'null', 'Null', 'NULL', '', None} NULL_VALUES = {'null', 'Null', 'NULL', '', None}
def to_internal_value(self, data): def to_internal_value(self, data):
try: with contextlib.suppress(TypeError):
if data in self.TRUE_VALUES: if data in self.TRUE_VALUES:
return True return True
elif data in self.FALSE_VALUES: elif data in self.FALSE_VALUES:
return False return False
elif data in self.NULL_VALUES and self.allow_null: elif data in self.NULL_VALUES and self.allow_null:
return None return None
except TypeError: # Input is an unhashable type
pass
self.fail('invalid', input=data) self.fail('invalid', input=data)
def to_representation(self, value): def to_representation(self, value):
@ -744,23 +710,6 @@ class BooleanField(Field):
return bool(value) return bool(value)
class NullBooleanField(BooleanField):
initial = None
def __init__(self, **kwargs):
warnings.warn(
"The `NullBooleanField` is deprecated and will be removed starting "
"with 3.14. Instead use the `BooleanField` field and set "
"`allow_null=True` which does the same thing.",
RemovedInDRF314Warning, stacklevel=2
)
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
kwargs['allow_null'] = True
super().__init__(**kwargs)
# String types... # String types...
class CharField(Field): class CharField(Field):
@ -1179,7 +1128,7 @@ class DateTimeField(Field):
When `self.default_timezone` is `None`, always return naive datetimes. When `self.default_timezone` is `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware datetimes. When `self.default_timezone` is not `None`, always return aware datetimes.
""" """
field_timezone = getattr(self, 'timezone', self.default_timezone()) field_timezone = self.timezone if hasattr(self, 'timezone') else self.default_timezone()
if field_timezone is not None: if field_timezone is not None:
if timezone.is_aware(value): if timezone.is_aware(value):
@ -1208,19 +1157,14 @@ class DateTimeField(Field):
return self.enforce_timezone(value) return self.enforce_timezone(value)
for input_format in input_formats: for input_format in input_formats:
if input_format.lower() == ISO_8601: with contextlib.suppress(ValueError, TypeError):
try: if input_format.lower() == ISO_8601:
parsed = parse_datetime(value) parsed = parse_datetime(value)
if parsed is not None: if parsed is not None:
return self.enforce_timezone(parsed) return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass parsed = self.datetime_parser(value, input_format)
else: return self.enforce_timezone(parsed)
try:
parsed = self.datetime_parser(value, input_format)
return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
humanized_format = humanize_datetime.datetime_formats(input_formats) humanized_format = humanize_datetime.datetime_formats(input_formats)
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
@ -1864,7 +1808,7 @@ class SerializerMethodField(Field):
For example: For example:
class ExampleSerializer(self): class ExampleSerializer(Serializer):
extra_info = SerializerMethodField() extra_info = SerializerMethodField()
def get_extra_info(self, obj): def get_extra_info(self, obj):

View File

@ -26,6 +26,7 @@ class Command(BaseCommand):
parser.add_argument('--urlconf', dest="urlconf", default=None, type=str) parser.add_argument('--urlconf', dest="urlconf", default=None, type=str)
parser.add_argument('--generator_class', dest="generator_class", default=None, type=str) parser.add_argument('--generator_class', dest="generator_class", default=None, type=str)
parser.add_argument('--file', dest="file", default=None, type=str) parser.add_argument('--file', dest="file", default=None, type=str)
parser.add_argument('--api_version', dest="api_version", default='', type=str)
def handle(self, *args, **options): def handle(self, *args, **options):
if options['generator_class']: if options['generator_class']:
@ -37,6 +38,7 @@ class Command(BaseCommand):
title=options['title'], title=options['title'],
description=options['description'], description=options['description'],
urlconf=options['urlconf'], urlconf=options['urlconf'],
version=options['api_version'],
) )
schema = generator.get_schema(request=None, public=True) schema = generator.get_schema(request=None, public=True)
renderer = self.get_renderer(options['format']) renderer = self.get_renderer(options['format'])

View File

@ -36,7 +36,6 @@ class SimpleMetadata(BaseMetadata):
label_lookup = ClassLookupDict({ label_lookup = ClassLookupDict({
serializers.Field: 'field', serializers.Field: 'field',
serializers.BooleanField: 'boolean', serializers.BooleanField: 'boolean',
serializers.NullBooleanField: 'boolean',
serializers.CharField: 'string', serializers.CharField: 'string',
serializers.UUIDField: 'string', serializers.UUIDField: 'string',
serializers.URLField: 'url', serializers.URLField: 'url',

View File

@ -4,7 +4,7 @@ incoming request. Typically this will be based on the request's Accept header.
""" """
from django.http import Http404 from django.http import Http404
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import exceptions
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import ( from rest_framework.utils.mediatypes import (
_MediaType, media_type_matches, order_by_precedence _MediaType, media_type_matches, order_by_precedence
@ -64,9 +64,11 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
full_media_type = ';'.join( full_media_type = ';'.join(
(renderer.media_type,) + (renderer.media_type,) +
tuple('{}={}'.format( tuple(
key, value.decode(HTTP_HEADER_ENCODING)) '{}={}'.format(key, value)
for key, value in media_type_wrapper.params.items())) for key, value in media_type_wrapper.params.items()
)
)
return renderer, full_media_type return renderer, full_media_type
else: else:
# Eg client requests 'application/json; indent=8' # Eg client requests 'application/json; indent=8'

View File

@ -2,6 +2,8 @@
Pagination serializers determine the structure of the output that should Pagination serializers determine the structure of the output that should
be used for paginated responses. be used for paginated responses.
""" """
import contextlib
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from urllib import parse from urllib import parse
@ -257,15 +259,12 @@ class PageNumberPagination(BasePagination):
def get_page_size(self, request): def get_page_size(self, request):
if self.page_size_query_param: if self.page_size_query_param:
try: with contextlib.suppress(KeyError, ValueError):
return _positive_int( return _positive_int(
request.query_params[self.page_size_query_param], request.query_params[self.page_size_query_param],
strict=True, strict=True,
cutoff=self.max_page_size cutoff=self.max_page_size
) )
except (KeyError, ValueError):
pass
return self.page_size return self.page_size
def get_next_link(self): def get_next_link(self):
@ -430,15 +429,12 @@ class LimitOffsetPagination(BasePagination):
def get_limit(self, request): def get_limit(self, request):
if self.limit_query_param: if self.limit_query_param:
try: with contextlib.suppress(KeyError, ValueError):
return _positive_int( return _positive_int(
request.query_params[self.limit_query_param], request.query_params[self.limit_query_param],
strict=True, strict=True,
cutoff=self.max_limit cutoff=self.max_limit
) )
except (KeyError, ValueError):
pass
return self.default_limit return self.default_limit
def get_offset(self, request): def get_offset(self, request):
@ -680,15 +676,12 @@ class CursorPagination(BasePagination):
def get_page_size(self, request): def get_page_size(self, request):
if self.page_size_query_param: if self.page_size_query_param:
try: with contextlib.suppress(KeyError, ValueError):
return _positive_int( return _positive_int(
request.query_params[self.page_size_query_param], request.query_params[self.page_size_query_param],
strict=True, strict=True,
cutoff=self.max_page_size cutoff=self.max_page_size
) )
except (KeyError, ValueError):
pass
return self.page_size return self.page_size
def get_next_link(self): def get_next_link(self):
@ -905,10 +898,16 @@ class CursorPagination(BasePagination):
'next': { 'next': {
'type': 'string', 'type': 'string',
'nullable': True, 'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{cursor_query_param}=cD00ODY%3D"'.format(
cursor_query_param=self.cursor_query_param)
}, },
'previous': { 'previous': {
'type': 'string', 'type': 'string',
'nullable': True, 'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{cursor_query_param}=cj0xJnA9NDg3'.format(
cursor_query_param=self.cursor_query_param)
}, },
'results': schema, 'results': schema,
}, },

View File

@ -4,8 +4,9 @@ Parsers are used to parse the content of incoming HTTP requests.
They give us a generic way of being able to handle various media types They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data. on the request, such as form content or json encoded data.
""" """
import codecs import codecs
from urllib import parse import contextlib
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
@ -13,10 +14,10 @@ from django.http import QueryDict
from django.http.multipartparser import ChunkIter from django.http.multipartparser import ChunkIter
from django.http.multipartparser import \ from django.http.multipartparser import \
MultiPartParser as DjangoMultiPartParser MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header from django.http.multipartparser import MultiPartParserError
from django.utils.encoding import force_str
from rest_framework import renderers from rest_framework import renderers
from rest_framework.compat import parse_header_parameters
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import json from rest_framework.utils import json
@ -194,30 +195,12 @@ class FileUploadParser(BaseParser):
Detects the uploaded file name. First searches a 'filename' url kwarg. Detects the uploaded file name. First searches a 'filename' url kwarg.
Then tries to parse Content-Disposition header. Then tries to parse Content-Disposition header.
""" """
try: with contextlib.suppress(KeyError):
return parser_context['kwargs']['filename'] return parser_context['kwargs']['filename']
except KeyError:
pass
try: with contextlib.suppress(AttributeError, KeyError, ValueError):
meta = parser_context['request'].META meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode()) disposition, params = parse_header_parameters(meta['HTTP_CONTENT_DISPOSITION'])
filename_parm = disposition[1] if 'filename*' in params:
if 'filename*' in filename_parm: return params['filename*']
return self.get_encoded_filename(filename_parm) return params['filename']
return force_str(filename_parm['filename'])
except (AttributeError, KeyError, ValueError):
pass
def get_encoded_filename(self, filename_parm):
"""
Handle encoded filenames per RFC6266. See also:
https://tools.ietf.org/html/rfc2231#section-4
"""
encoded_filename = force_str(filename_parm['filename*'])
try:
charset, lang, filename = encoded_filename.split('\'', 2)
filename = parse.unquote(filename)
except (ValueError, LookupError):
filename = force_str(filename_parm['filename'])
return filename

View File

@ -46,6 +46,14 @@ class OperandHolder(OperationHolderMixin):
op2 = self.op2_class(*args, **kwargs) op2 = self.op2_class(*args, **kwargs)
return self.operator_class(op1, op2) return self.operator_class(op1, op2)
def __eq__(self, other):
return (
isinstance(other, OperandHolder) and
self.operator_class == other.operator_class and
self.op1_class == other.op1_class and
self.op2_class == other.op2_class
)
class AND: class AND:
def __init__(self, op1, op2): def __init__(self, op1, op2):
@ -78,8 +86,11 @@ class OR:
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return ( return (
self.op1.has_object_permission(request, view, obj) or self.op1.has_permission(request, view)
self.op2.has_object_permission(request, view, obj) and self.op1.has_object_permission(request, view, obj)
) or (
self.op2.has_permission(request, view)
and self.op2.has_object_permission(request, view, obj)
) )

View File

@ -1,3 +1,4 @@
import contextlib
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from urllib import parse from urllib import parse
@ -170,7 +171,7 @@ class RelatedField(Field):
def get_attribute(self, instance): def get_attribute(self, instance):
if self.use_pk_only_optimization() and self.source_attrs: if self.use_pk_only_optimization() and self.source_attrs:
# Optimized case, return a mock object only containing the pk attribute. # Optimized case, return a mock object only containing the pk attribute.
try: with contextlib.suppress(AttributeError):
attribute_instance = get_attribute(instance, self.source_attrs[:-1]) attribute_instance = get_attribute(instance, self.source_attrs[:-1])
value = attribute_instance.serializable_value(self.source_attrs[-1]) value = attribute_instance.serializable_value(self.source_attrs[-1])
if is_simple_callable(value): if is_simple_callable(value):
@ -183,9 +184,6 @@ class RelatedField(Field):
value = getattr(value, 'pk', value) value = getattr(value, 'pk', value)
return PKOnlyObject(pk=value) return PKOnlyObject(pk=value)
except AttributeError:
pass
# Standard case, return the object instance. # Standard case, return the object instance.
return super().get_attribute(instance) return super().get_attribute(instance)

View File

@ -6,7 +6,9 @@ on the response, such as JSON encoded data or HTML output.
REST framework also provides an HTML renderer that renders the browsable API. REST framework also provides an HTML renderer that renders the browsable API.
""" """
import base64 import base64
import contextlib
from collections import OrderedDict from collections import OrderedDict
from urllib import parse from urllib import parse
@ -14,7 +16,6 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import engines, loader from django.template import engines, loader
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils.html import mark_safe from django.utils.html import mark_safe
@ -22,7 +23,7 @@ from django.utils.html import mark_safe
from rest_framework import VERSION, exceptions, serializers, status from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import ( from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
pygments_css, yaml parse_header_parameters, pygments_css, yaml
) )
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method from rest_framework.request import is_form_media_type, override_method
@ -72,12 +73,9 @@ class JSONRenderer(BaseRenderer):
# If the media type looks like 'application/json; indent=4', # If the media type looks like 'application/json; indent=4',
# then pretty print the result. # then pretty print the result.
# Note that we coerce `indent=0` into `indent=None`. # Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii')) base_media_type, params = parse_header_parameters(accepted_media_type)
try: with contextlib.suppress(KeyError, ValueError, TypeError):
return zero_as_none(max(min(int(params['indent']), 8), 0)) return zero_as_none(max(min(int(params['indent']), 8), 0))
except (KeyError, ValueError, TypeError):
pass
# If 'indent' is provided in the context, then pretty print the result. # If 'indent' is provided in the context, then pretty print the result.
# E.g. If we're being called by the BrowsableAPIRenderer. # E.g. If we're being called by the BrowsableAPIRenderer.
return renderer_context.get('indent', None) return renderer_context.get('indent', None)
@ -489,11 +487,8 @@ class BrowsableAPIRenderer(BaseRenderer):
return return
if existing_serializer is not None: if existing_serializer is not None:
try: with contextlib.suppress(TypeError):
return self.render_form_for_serializer(existing_serializer) return self.render_form_for_serializer(existing_serializer)
except TypeError:
pass
if has_serializer: if has_serializer:
if method in ('PUT', 'PATCH'): if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, **kwargs) serializer = view.get_serializer(instance=instance, **kwargs)

View File

@ -14,11 +14,11 @@ from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.http.multipartparser import parse_header
from django.http.request import RawPostDataException from django.http.request import RawPostDataException
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import exceptions
from rest_framework.compat import parse_header_parameters
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -26,7 +26,7 @@ def is_form_media_type(media_type):
""" """
Return True if the media type is a valid form media type. Return True if the media type is a valid form media type.
""" """
base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) base_media_type, params = parse_header_parameters(media_type)
return (base_media_type == 'application/x-www-form-urlencoded' or return (base_media_type == 'application/x-www-form-urlencoded' or
base_media_type == 'multipart/form-data') base_media_type == 'multipart/form-data')
@ -413,7 +413,8 @@ class Request:
to proxy it to the underlying HttpRequest object. to proxy it to the underlying HttpRequest object.
""" """
try: try:
return getattr(self._request, attr) _request = self.__getattribute__("_request")
return getattr(_request, attr)
except AttributeError: except AttributeError:
return self.__getattribute__(attr) return self.__getattribute__(attr)

View File

@ -198,7 +198,11 @@ class SchemaGenerator(BaseSchemaGenerator):
if is_custom_action(action): if is_custom_action(action):
# Custom action, eg "/users/{pk}/activate/", "/users/active/" # Custom action, eg "/users/{pk}/activate/", "/users/active/"
if len(view.action_map) > 1: mapped_methods = {
# Don't count head mapping, e.g. not part of the schema
method for method in view.action_map if method != 'head'
}
if len(mapped_methods) > 1:
action = self.default_mapping[method.lower()] action = self.default_mapping[method.lower()]
if action in self.coerce_method_names: if action in self.coerce_method_names:
action = self.coerce_method_names[action] action = self.coerce_method_names[action]

View File

@ -88,7 +88,7 @@ class ViewInspector:
view.get_view_description()) view.get_view_description())
def _get_description_section(self, view, header, description): def _get_description_section(self, view, header, description):
lines = [line for line in description.splitlines()] lines = description.splitlines()
current_section = '' current_section = ''
sections = {'': ''} sections = {'': ''}

View File

@ -13,7 +13,7 @@ from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework import ( from rest_framework import (
RemovedInDRF314Warning, exceptions, renderers, serializers RemovedInDRF315Warning, exceptions, renderers, serializers
) )
from rest_framework.compat import uritemplate from rest_framework.compat import uritemplate
from rest_framework.fields import _UnvalidatedField, empty from rest_framework.fields import _UnvalidatedField, empty
@ -523,7 +523,7 @@ class AutoSchema(ViewInspector):
continue continue
if field.required: if field.required:
required.append(field.field_name) required.append(self.get_field_name(field))
schema = self.map_field(field) schema = self.map_field(field)
if field.read_only: if field.read_only:
@ -538,7 +538,7 @@ class AutoSchema(ViewInspector):
schema['description'] = str(field.help_text) schema['description'] = str(field.help_text)
self.map_field_validators(field, schema) self.map_field_validators(field, schema)
properties[field.field_name] = schema properties[self.get_field_name(field)] = schema
result = { result = {
'type': 'object', 'type': 'object',
@ -589,6 +589,13 @@ class AutoSchema(ViewInspector):
schema['maximum'] = int(digits * '9') + 1 schema['maximum'] = int(digits * '9') + 1
schema['minimum'] = -schema['maximum'] schema['minimum'] = -schema['maximum']
def get_field_name(self, field):
"""
Override this method if you want to change schema field name.
For example, convert snake_case field name to camelCase.
"""
return field.field_name
def get_paginator(self): def get_paginator(self):
pagination_class = getattr(self.view, 'pagination_class', None) pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class: if pagination_class:
@ -713,106 +720,10 @@ class AutoSchema(ViewInspector):
return [path.split('/')[0].replace('_', '-')] return [path.split('/')[0].replace('_', '-')]
def _get_path_parameters(self, path, method):
warnings.warn(
"Method `_get_path_parameters()` has been renamed to `get_path_parameters()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_path_parameters(path, method)
def _get_filter_parameters(self, path, method):
warnings.warn(
"Method `_get_filter_parameters()` has been renamed to `get_filter_parameters()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_filter_parameters(path, method)
def _get_responses(self, path, method):
warnings.warn(
"Method `_get_responses()` has been renamed to `get_responses()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_responses(path, method)
def _get_request_body(self, path, method):
warnings.warn(
"Method `_get_request_body()` has been renamed to `get_request_body()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_request_body(path, method)
def _get_serializer(self, path, method):
warnings.warn(
"Method `_get_serializer()` has been renamed to `get_serializer()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_serializer(path, method)
def _get_paginator(self):
warnings.warn(
"Method `_get_paginator()` has been renamed to `get_paginator()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_paginator()
def _map_field_validators(self, field, schema):
warnings.warn(
"Method `_map_field_validators()` has been renamed to `map_field_validators()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.map_field_validators(field, schema)
def _map_serializer(self, serializer):
warnings.warn(
"Method `_map_serializer()` has been renamed to `map_serializer()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.map_serializer(serializer)
def _map_field(self, field):
warnings.warn(
"Method `_map_field()` has been renamed to `map_field()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.map_field(field)
def _map_choicefield(self, field):
warnings.warn(
"Method `_map_choicefield()` has been renamed to `map_choicefield()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.map_choicefield(field)
def _get_pagination_parameters(self, path, method):
warnings.warn(
"Method `_get_pagination_parameters()` has been renamed to `get_pagination_parameters()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_pagination_parameters(path, method)
def _allows_filters(self, path, method):
warnings.warn(
"Method `_allows_filters()` has been renamed to `allows_filters()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.allows_filters(path, method)
def _get_reference(self, serializer): def _get_reference(self, serializer):
warnings.warn( warnings.warn(
"Method `_get_reference()` has been renamed to `get_reference()`. " "Method `_get_reference()` has been renamed to `get_reference()`. "
"The old name will be removed in DRF v3.14.", "The old name will be removed in DRF v3.15.",
RemovedInDRF314Warning, stacklevel=2 RemovedInDRF315Warning, stacklevel=2
) )
return self.get_reference(serializer) return self.get_reference(serializer)

View File

@ -10,6 +10,8 @@ python primitives.
2. The process of marshalling between python primitives and request and 2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers. response content is handled by parsers and renderers.
""" """
import contextlib
import copy import copy
import inspect import inspect
import traceback import traceback
@ -52,7 +54,7 @@ from rest_framework.fields import ( # NOQA # isort:skip
BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField, BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField,
DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField, DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField,
HiddenField, HStoreField, IPAddressField, ImageField, IntegerField, JSONField, HiddenField, HStoreField, IPAddressField, ImageField, IntegerField, JSONField,
ListField, ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField, ListField, ModelField, MultipleChoiceField, ReadOnlyField,
RegexField, SerializerMethodField, SlugField, TimeField, URLField, UUIDField, RegexField, SerializerMethodField, SlugField, TimeField, URLField, UUIDField,
) )
from rest_framework.relations import ( # NOQA # isort:skip from rest_framework.relations import ( # NOQA # isort:skip
@ -216,7 +218,7 @@ class BaseSerializer(Field):
return self.instance return self.instance
def is_valid(self, raise_exception=False): def is_valid(self, *, raise_exception=False):
assert hasattr(self, 'initial_data'), ( assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'Cannot call `.is_valid()` as no `data=` keyword argument was '
'passed when instantiating the serializer instance.' 'passed when instantiating the serializer instance.'
@ -735,7 +737,7 @@ class ListSerializer(BaseSerializer):
return self.instance return self.instance
def is_valid(self, raise_exception=False): def is_valid(self, *, raise_exception=False):
# This implementation is the same as the default, # This implementation is the same as the default,
# except that we use lists, rather than dicts, as the empty case. # except that we use lists, rather than dicts, as the empty case.
assert hasattr(self, 'initial_data'), ( assert hasattr(self, 'initial_data'), (
@ -1496,12 +1498,10 @@ class ModelSerializer(Serializer):
# they can't be nested attribute lookups. # they can't be nested attribute lookups.
continue continue
try: with contextlib.suppress(FieldDoesNotExist):
field = model._meta.get_field(source) field = model._meta.get_field(source)
if isinstance(field, DjangoModelField): if isinstance(field, DjangoModelField):
model_fields[source] = field model_fields[source] = field
except FieldDoesNotExist:
pass
return model_fields return model_fields

View File

@ -19,7 +19,9 @@ REST framework settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed # Import from `django.core.signals` instead of the official location
# `django.test.signals` to avoid importing the test module unnecessarily.
from django.core.signals import setting_changed
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from rest_framework import ISO_8601 from rest_framework import ISO_8601

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

File diff suppressed because one or more lines are too long

View File

@ -131,13 +131,7 @@ $(function () {
if (value !== undefined) { if (value !== undefined) {
params[paramKey] = value params[paramKey] = value
} }
} else if (dataType === 'array' && paramValue) { } else if ((dataType === 'array' && paramValue) || (dataType === 'object' && paramValue)) {
try {
params[paramKey] = JSON.parse(paramValue)
} catch (err) {
// Ignore malformed JSON
}
} else if (dataType === 'object' && paramValue) {
try { try {
params[paramKey] = JSON.parse(paramValue) params[paramKey] = JSON.parse(paramValue)
} catch (err) { } catch (err) {

View File

@ -277,7 +277,7 @@ class APIClient(APIRequestFactory, DjangoClient):
""" """
self.handler._force_user = user self.handler._force_user = user
self.handler._force_token = token self.handler._force_token = token
if user is None: if user is None and token is None:
self.logout() # Also clear any possible session info if required self.logout() # Also clear any possible session info if required
def request(self, **kwargs): def request(self, **kwargs):

View File

@ -171,7 +171,7 @@ class AnonRateThrottle(SimpleRateThrottle):
scope = 'anon' scope = 'anon'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
return None # Only throttle unauthenticated requests. return None # Only throttle unauthenticated requests.
return self.cache_format % { return self.cache_format % {
@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):
scope = 'user' scope = 'user'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)
@ -237,9 +237,9 @@ class ScopedRateThrottle(SimpleRateThrottle):
If `view.throttle_scope` is not set, don't apply this throttle. If `view.throttle_scope` is not set, don't apply this throttle.
Otherwise generate the unique cache key by concatenating the user id Otherwise generate the unique cache key by concatenating the user id
with the '.throttle_scope` property of the view. with the `.throttle_scope` property of the view.
""" """
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)

View File

@ -1,6 +1,8 @@
""" """
Helper classes for parsers. Helper classes for parsers.
""" """
import contextlib
import datetime import datetime
import decimal import decimal
import json # noqa import json # noqa
@ -58,10 +60,8 @@ class JSONEncoder(json.JSONEncoder):
) )
elif hasattr(obj, '__getitem__'): elif hasattr(obj, '__getitem__'):
cls = (list if isinstance(obj, (list, tuple)) else dict) cls = (list if isinstance(obj, (list, tuple)) else dict)
try: with contextlib.suppress(Exception):
return cls(obj) return cls(obj)
except Exception:
pass
elif hasattr(obj, '__iter__'): elif hasattr(obj, '__iter__'):
return tuple(item for item in obj) return tuple(item for item in obj)
return super().default(obj) return super().default(obj)

View File

@ -95,6 +95,9 @@ def get_field_kwargs(field_name, model_field):
(hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)): (hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)):
kwargs['style'] = {'base_template': 'textarea.html'} kwargs['style'] = {'base_template': 'textarea.html'}
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field, models.AutoField) or not model_field.editable: if isinstance(model_field, models.AutoField) or not model_field.editable:
# If this field is read-only, then return early. # If this field is read-only, then return early.
# Further keyword arguments are not valid. # Further keyword arguments are not valid.
@ -104,9 +107,6 @@ def get_field_kwargs(field_name, model_field):
if model_field.has_default() or model_field.blank or model_field.null: if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False kwargs['required'] = False
if model_field.null:
kwargs['allow_null'] = True
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))): if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
kwargs['allow_blank'] = True kwargs['allow_blank'] = True
@ -263,6 +263,8 @@ def get_relation_kwargs(field_name, relation_info):
if not model_field.editable: if not model_field.editable:
kwargs['read_only'] = True kwargs['read_only'] = True
kwargs.pop('queryset', None) kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if kwargs.get('read_only', False): if kwargs.get('read_only', False):
# If this field is read-only, then return early. # If this field is read-only, then return early.
# No further keyword arguments are valid. # No further keyword arguments are valid.
@ -270,8 +272,6 @@ def get_relation_kwargs(field_name, relation_info):
if model_field.has_default() or model_field.blank or model_field.null: if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False kwargs['required'] = False
if model_field.null:
kwargs['allow_null'] = True
if model_field.validators: if model_field.validators:
kwargs['validators'] = model_field.validators kwargs['validators'] = model_field.validators
if getattr(model_field, 'unique', False): if getattr(model_field, 'unique', False):

View File

@ -3,9 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
""" """
from django.http.multipartparser import parse_header from rest_framework.compat import parse_header_parameters
from rest_framework import HTTP_HEADER_ENCODING
def media_type_matches(lhs, rhs): def media_type_matches(lhs, rhs):
@ -46,7 +44,7 @@ def order_by_precedence(media_type_lst):
class _MediaType: class _MediaType:
def __init__(self, media_type_str): def __init__(self, media_type_str):
self.orig = '' if (media_type_str is None) else media_type_str self.orig = '' if (media_type_str is None) else media_type_str
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING)) self.full_type, self.params = parse_header_parameters(self.orig)
self.main_type, sep, self.sub_type = self.full_type.partition('/') self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other): def match(self, other):
@ -79,5 +77,5 @@ class _MediaType:
def __str__(self): def __str__(self):
ret = "%s/%s" % (self.main_type, self.sub_type) ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items(): for key, val in self.params.items():
ret += "; %s=%s" % (key, val.decode('ascii')) ret += "; %s=%s" % (key, val)
return ret return ret

View File

@ -1,3 +1,4 @@
import contextlib
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Mapping, MutableMapping from collections.abc import Mapping, MutableMapping
@ -103,15 +104,13 @@ class JSONBoundField(BoundField):
# When HTML form input is used and the input is not valid # When HTML form input is used and the input is not valid
# value will be a JSONString, rather than a JSON primitive. # value will be a JSONString, rather than a JSON primitive.
if not getattr(value, 'is_json_string', False): if not getattr(value, 'is_json_string', False):
try: with contextlib.suppress(TypeError, ValueError):
value = json.dumps( value = json.dumps(
self.value, self.value,
sort_keys=True, sort_keys=True,
indent=4, indent=4,
separators=(',', ': '), separators=(',', ': '),
) )
except (TypeError, ValueError):
pass
return self.__class__(self._field, value, self.errors, self._prefix) return self.__class__(self._field, value, self.errors, self._prefix)

View File

@ -79,9 +79,9 @@ def exception_handler(exc, context):
to be raised. to be raised.
""" """
if isinstance(exc, Http404): if isinstance(exc, Http404):
exc = exceptions.NotFound() exc = exceptions.NotFound(*(exc.args))
elif isinstance(exc, PermissionDenied): elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied() exc = exceptions.PermissionDenied(*(exc.args))
if isinstance(exc, exceptions.APIException): if isinstance(exc, exceptions.APIException):
headers = {} headers = {}

View File

@ -198,6 +198,10 @@ class ViewSetMixin:
for action in actions: for action in actions:
try: try:
url_name = '%s-%s' % (self.basename, action.url_name) url_name = '%s-%s' % (self.basename, action.url_name)
namespace = self.request.resolver_match.namespace
if namespace:
url_name = '%s:%s' % (namespace, url_name)
url = reverse(url_name, self.args, self.kwargs, request=self.request) url = reverse(url_name, self.args, self.kwargs, request=self.request)
view = self.__class__(**action.kwargs) view = self.__class__(**action.kwargs)
action_urls[view.get_view_name()] = url action_urls[view.get_view_name()] = url

View File

@ -1,11 +1,11 @@
[metadata] [metadata]
license_file = LICENSE.md license_files = LICENSE.md
[tool:pytest] [tool:pytest]
addopts=--tb=short --strict-markers -ra addopts=--tb=short --strict-markers -ra
[flake8] [flake8]
ignore = E501,W504 ignore = E501,W503,W504
banned-modules = json = use from rest_framework.utils import json! banned-modules = json = use from rest_framework.utils import json!
[isort] [isort]

View File

@ -82,14 +82,13 @@ setup(
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
packages=find_packages(exclude=['tests*']), packages=find_packages(exclude=['tests*']),
include_package_data=True, include_package_data=True,
install_requires=["django>=2.2", "pytz"], install_requires=["django>=3.0", "pytz"],
python_requires=">=3.6", python_requires=">=3.6",
zip_safe=False, zip_safe=False,
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0', 'Framework :: Django :: 3.0',
'Framework :: Django :: 3.1', 'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2', 'Framework :: Django :: 3.2',

View File

@ -219,8 +219,8 @@ class SessionAuthTests(TestCase):
Ensure POSTing form over session authentication with CSRF token succeeds. Ensure POSTing form over session authentication with CSRF token succeeds.
Regression test for #6088 Regression test for #6088
""" """
# Remove this shim when dropping support for Django 2.2. # Remove this shim when dropping support for Django 3.0.
if django.VERSION < (3, 0): if django.VERSION < (3, 1):
from django.middleware.csrf import _get_new_csrf_token from django.middleware.csrf import _get_new_csrf_token
else: else:
from django.middleware.csrf import ( from django.middleware.csrf import (

View File

@ -754,6 +754,67 @@ class TestSchemaGeneratorWithManyToMany(TestCase):
assert schema == expected assert schema == expected
@unittest.skipUnless(coreapi, 'coreapi is not installed')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
class TestSchemaGeneratorActionKeysViewSets(TestCase):
def test_action_not_coerced_for_get_and_head(self):
"""
Ensure that action name is preserved when action map contains "head".
"""
class CustomViewSet(GenericViewSet):
serializer_class = EmptySerializer
@action(methods=['get', 'head'], detail=True)
def custom_read(self, request, pk):
raise NotImplementedError
@action(methods=['put', 'patch'], detail=True)
def custom_mixed_update(self, request, pk):
raise NotImplementedError
self.router = DefaultRouter()
self.router.register('example', CustomViewSet, basename='example')
self.patterns = [
path('', include(self.router.urls))
]
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
schema = generator.get_schema()
expected = coreapi.Document(
url='',
title='Example API',
content={
'example': {
'custom_read': coreapi.Link(
url='/example/{id}/custom_read/',
action='get',
fields=[
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
]
),
'custom_mixed_update': {
'update': coreapi.Link(
url='/example/{id}/custom_mixed_update/',
action='put',
fields=[
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
]
),
'partial_update': coreapi.Link(
url='/example/{id}/custom_mixed_update/',
action='patch',
fields=[
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
]
)
}
}
}
)
assert schema == expected
@unittest.skipUnless(coreapi, 'coreapi is not installed') @unittest.skipUnless(coreapi, 'coreapi is not installed')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'}) @override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
class Test4605Regression(TestCase): class Test4605Regression(TestCase):

View File

@ -52,9 +52,9 @@ class GenerateSchemaTests(TestCase):
@pytest.mark.skipif(yaml is None, reason='PyYAML is required.') @pytest.mark.skipif(yaml is None, reason='PyYAML is required.')
def test_renders_default_schema_with_custom_title_url_and_description(self): def test_renders_default_schema_with_custom_title_url_and_description(self):
call_command('generateschema', call_command('generateschema',
'--title=SampleAPI', '--title=ExampleAPI',
'--url=http://api.sample.com', '--url=http://api.example.com',
'--description=Sample description', '--description=Example description',
stdout=self.out) stdout=self.out)
# Check valid YAML was output. # Check valid YAML was output.
schema = yaml.safe_load(self.out.getvalue()) schema = yaml.safe_load(self.out.getvalue())
@ -94,8 +94,8 @@ class GenerateSchemaTests(TestCase):
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'}) @override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
def test_coreapi_renders_default_schema_with_custom_title_url_and_description(self): def test_coreapi_renders_default_schema_with_custom_title_url_and_description(self):
expected_out = """info: expected_out = """info:
description: Sample description description: Example description
title: SampleAPI title: ExampleAPI
version: '' version: ''
openapi: 3.0.0 openapi: 3.0.0
paths: paths:
@ -103,12 +103,12 @@ class GenerateSchemaTests(TestCase):
get: get:
operationId: list operationId: list
servers: servers:
- url: http://api.sample.com/ - url: http://api.example.com/
""" """
call_command('generateschema', call_command('generateschema',
'--title=SampleAPI', '--title=ExampleAPI',
'--url=http://api.sample.com', '--url=http://api.example.com',
'--description=Sample description', '--description=Example description',
stdout=self.out) stdout=self.out)
self.assertIn(formatting.dedent(expected_out), self.out.getvalue()) self.assertIn(formatting.dedent(expected_out), self.out.getvalue())

View File

@ -111,6 +111,20 @@ class TestFieldMapping(TestCase):
assert data['properties']['default_false']['default'] is False, "default must be false" assert data['properties']['default_false']['default'] is False, "default must be false"
assert 'default' not in data['properties']['without_default'], "default must not be defined" assert 'default' not in data['properties']['without_default'], "default must not be defined"
def test_custom_field_name(self):
class CustomSchema(AutoSchema):
def get_field_name(self, field):
return 'custom_' + field.field_name
class Serializer(serializers.Serializer):
text_field = serializers.CharField()
inspector = CustomSchema()
data = inspector.map_serializer(Serializer())
assert 'custom_text_field' in data['properties']
assert 'text_field' not in data['properties']
def test_nullable_fields(self): def test_nullable_fields(self):
class Model(models.Model): class Model(models.Model):
rw_field = models.CharField(null=True) rw_field = models.CharField(null=True)

View File

@ -1,5 +1,3 @@
import sys
import pytest import pytest
from django.test import TestCase from django.test import TestCase
@ -33,7 +31,7 @@ indented
# If markdown is installed we also test it's working # If markdown is installed we also test it's working
# (and that our wrapped forces '=' to h2 and '-' to h3) # (and that our wrapped forces '=' to h2 and '-' to h3)
MARKDOWN_BASE = """<h2 id="an-example-docstring">an example docstring</h2> MARKDOWN_DOCSTRING = """<h2 id="an-example-docstring">an example docstring</h2>
<ul> <ul>
<li>list</li> <li>list</li>
<li>list</li> <li>list</li>
@ -42,25 +40,8 @@ MARKDOWN_BASE = """<h2 id="an-example-docstring">an example docstring</h2>
<pre><code>code block <pre><code>code block
</code></pre> </code></pre>
<p>indented</p> <p>indented</p>
<h2 id="hash-style-header">hash style header</h2>%s""" <h2 id="hash-style-header">hash style header</h2>
<div class="highlight"><pre><span></span><span class="p">[{</span><span class="w"></span><br /><span class="w"> </span><span class="nt">&quot;alpha&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span><br /><span class="w"> </span><span class="nt">&quot;beta&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;this is a string&quot;</span><span class="w"></span><br /><span class="p">}]</span><span class="w"></span><br /></pre></div>
MARKDOWN_gte_33 = """
<div class="highlight"><pre><span></span><span class="p">[{</span><br />\
<span class="nt">&quot;alpha&quot;</span><span class="p">:</span>\
<span class="mi">1</span><span class="p">,</span><br />\
<span class="nt">&quot;beta&quot;</span><span class="p">:</span>\
<span class="s2">&quot;this is a string&quot;</span><br />\
<span class="p">}]</span><br /></pre></div>
<p><br /></p>"""
MARKDOWN_lt_33 = """
<div class="highlight"><pre><span></span><span class="p">[{</span><br />\
<span class="nt">&quot;alpha&quot;</span><span class="p">:</span>\
<span class="mi">1</span><span class="p">,</span><br />\
<span class="nt">&quot;beta&quot;</span><span class="p">:</span>\
<span class="s2">&quot;this is a string&quot;</span><br />\
<span class="p">}]</span><br /></pre></div>
<p><br /></p>""" <p><br /></p>"""
@ -163,11 +144,7 @@ class TestViewNamesAndDescriptions(TestCase):
""" """
Ensure markdown to HTML works as expected. Ensure markdown to HTML works as expected.
""" """
# Markdown 3.3 is only supported on Python 3.6 and higher assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
if sys.version_info >= (3, 6):
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33
else:
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33
def test_dedent_tabs(): def test_dedent_tabs():

View File

@ -566,7 +566,7 @@ class TestCreateOnlyDefault:
def test_create_only_default_callable_sets_context(self): def test_create_only_default_callable_sets_context(self):
""" """
CreateOnlyDefault instances with a callable default should set_context CreateOnlyDefault instances with a callable default should set context
on the callable if possible on the callable if possible
""" """
class TestCallableDefault: class TestCallableDefault:
@ -679,9 +679,9 @@ class TestBooleanField(FieldValues):
assert exc_info.value.detail == expected assert exc_info.value.detail == expected
class TestNullBooleanField(TestBooleanField): class TestNullableBooleanField(TestBooleanField):
""" """
Valid and invalid values for `NullBooleanField`. Valid and invalid values for `BooleanField` when `allow_null=True`.
""" """
valid_inputs = { valid_inputs = {
'true': True, 'true': True,
@ -706,16 +706,6 @@ class TestNullBooleanField(TestBooleanField):
field = serializers.BooleanField(allow_null=True) field = serializers.BooleanField(allow_null=True)
class TestNullableBooleanField(TestNullBooleanField):
"""
Valid and invalid values for `BooleanField` when `allow_null=True`.
"""
@property
def field(self):
return serializers.BooleanField(allow_null=True)
# String types... # String types...
class TestCharField(FieldValues): class TestCharField(FieldValues):

View File

@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, renderers, serializers, status from rest_framework import generics, renderers, serializers, status
from rest_framework.exceptions import ErrorDetail
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from tests.models import ( from tests.models import (
@ -519,7 +520,12 @@ class TestFilterBackendAppliedToViews(TestCase):
request = factory.get('/1') request = factory.get('/1')
response = instance_view(request, pk=1).render() response = instance_view(request, pk=1).render()
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.data == {'detail': 'Not found.'} assert response.data == {
'detail': ErrorDetail(
string='No BasicModel matches the given query.',
code='not_found'
)
}
def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self):
""" """

View File

@ -922,10 +922,14 @@ class CursorPaginationTestsMixin:
'next': { 'next': {
'type': 'string', 'type': 'string',
'nullable': True, 'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?cursor=cD00ODY%3D"'
}, },
'previous': { 'previous': {
'type': 'string', 'type': 'string',
'nullable': True, 'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?cursor=cj0xJnA9NDg3'
}, },
'results': unpaginated_schema, 'results': unpaginated_schema,
}, },

View File

@ -635,7 +635,7 @@ class PermissionsCompositionTests(TestCase):
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None) hasperm = composed_perm().has_object_permission(request, None, None)
assert hasperm is True assert hasperm is True
assert mock_deny.call_count == 1 assert mock_deny.call_count == 0
assert mock_allow.call_count == 1 assert mock_allow.call_count == 1
def test_and_lazyness(self): def test_and_lazyness(self):
@ -677,3 +677,16 @@ class PermissionsCompositionTests(TestCase):
assert hasperm is False assert hasperm is False
assert mock_deny.call_count == 1 assert mock_deny.call_count == 1
mock_allow.assert_not_called() mock_allow.assert_not_called()
def test_unimplemented_has_object_permission(self):
"test for issue 6402 https://github.com/encode/django-rest-framework/issues/6402"
request = factory.get('/1', format='json')
request.user = AnonymousUser()
class IsAuthenticatedUserOwner(permissions.IsAuthenticated):
def has_object_permission(self, request, view, obj):
return True
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
hasperm = composed_perm().has_object_permission(request, None, None)
assert hasperm is False

View File

@ -1,6 +1,7 @@
""" """
Tests for content parsing, and form-overloaded content parsing. Tests for content parsing, and form-overloaded content parsing.
""" """
import copy
import os.path import os.path
import tempfile import tempfile
@ -344,3 +345,10 @@ class TestHttpRequest(TestCase):
# ensure that request stream was consumed by form parser # ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data') assert request.content_type.startswith('multipart/form-data')
assert response.data == {'a': ['b']} assert response.data == {'a': ['b']}
class TestDeepcopy(TestCase):
def test_deepcopy_works(self):
request = Request(factory.get('/', secure=False))
copy.deepcopy(request)

View File

@ -10,6 +10,7 @@ from django.test import TestCase, override_settings
from django.urls import path from django.urls import path
from rest_framework import fields, serializers from rest_framework import fields, serializers
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.test import ( from rest_framework.test import (
@ -19,10 +20,12 @@ from rest_framework.test import (
@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) @api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
def view(request): def view(request):
return Response({ data = {'auth': request.META.get('HTTP_AUTHORIZATION', b'')}
'auth': request.META.get('HTTP_AUTHORIZATION', b''), if request.user:
'user': request.user.username data['user'] = request.user.username
}) if request.auth:
data['token'] = request.auth.key
return Response(data)
@api_view(['GET', 'POST']) @api_view(['GET', 'POST'])
@ -78,14 +81,46 @@ class TestAPITestClient(TestCase):
response = self.client.get('/view/') response = self.client.get('/view/')
assert response.data['auth'] == 'example' assert response.data['auth'] == 'example'
def test_force_authenticate(self): def test_force_authenticate_with_user(self):
""" """
Setting `.force_authenticate()` forcibly authenticates each request. Setting `.force_authenticate()` with a user forcibly authenticates each
request with that user.
""" """
user = User.objects.create_user('example', 'example@example.com') user = User.objects.create_user('example', 'example@example.com')
self.client.force_authenticate(user)
self.client.force_authenticate(user=user)
response = self.client.get('/view/') response = self.client.get('/view/')
assert response.data['user'] == 'example' assert response.data['user'] == 'example'
assert 'token' not in response.data
def test_force_authenticate_with_token(self):
"""
Setting `.force_authenticate()` with a token forcibly authenticates each
request with that token.
"""
user = User.objects.create_user('example', 'example@example.com')
token = Token.objects.create(key='xyz', user=user)
self.client.force_authenticate(token=token)
response = self.client.get('/view/')
assert response.data['token'] == 'xyz'
assert 'user' not in response.data
def test_force_authenticate_with_user_and_token(self):
"""
Setting `.force_authenticate()` with a user and token forcibly
authenticates each request with that user and token.
"""
user = User.objects.create_user('example', 'example@example.com')
token = Token.objects.create(key='xyz', user=user)
self.client.force_authenticate(user=user, token=token)
response = self.client.get('/view/')
assert response.data['user'] == 'example'
assert response.data['token'] == 'xyz'
def test_force_authenticate_with_sessions(self): def test_force_authenticate_with_sessions(self):
""" """
@ -102,8 +137,9 @@ class TestAPITestClient(TestCase):
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
assert response.data['active_session'] is True assert response.data['active_session'] is True
# Force authenticating as `None` should also logout the user session. # Force authenticating with `None` user and token should also logout
self.client.force_authenticate(None) # the user session.
self.client.force_authenticate(user=None, token=None)
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
assert response.data['active_session'] is False assert response.data['active_session'] is False

View File

@ -1,6 +1,6 @@
[tox] [tox]
envlist = envlist =
{py36,py37,py38,py39}-django22, {py36,py37,py38,py39}-django30,
{py36,py37,py38,py39}-django31, {py36,py37,py38,py39}-django31,
{py36,py37,py38,py39,py310}-django32, {py36,py37,py38,py39,py310}-django32,
{py38,py39,py310}-{django40,django41,djangomain}, {py38,py39,py310}-{django40,django41,djangomain},
@ -8,7 +8,7 @@ envlist =
[travis:env] [travis:env]
DJANGO = DJANGO =
2.2: django22 3.0: django30
3.1: django31 3.1: django31
3.2: django32 3.2: django32
4.0: django40 4.0: django40
@ -22,7 +22,7 @@ setenv =
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once PYTHONWARNINGS=once
deps = deps =
django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1
django31: Django>=3.1,<3.2 django31: Django>=3.1,<3.2
django32: Django>=3.2,<4.0 django32: Django>=3.2,<4.0
django40: Django>=4.0,<4.1 django40: Django>=4.0,<4.1