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 LICENSE.md
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/locale *.mo
global-exclude __pycache__

View File

@ -54,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements
* Python (3.6, 3.7, 3.8, 3.9, 3.10)
* Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1)
* Python 3.6+
* Django 4.1, 4.0, 3.2, 3.1, 3.0
We **highly recommend** and only officially support the latest patch release of
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:
```python
from django.urls import path, include
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.
class UserSerializer(serializers.HyperlinkedModelSerializer):
@ -111,7 +112,6 @@ class UserViewSet(viewsets.ModelViewSet):
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
@ -185,7 +185,7 @@ Please see the [security policy][security-policy].
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
[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
[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.
@ -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():
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:
@ -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`.
@ -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**.
#### Installation & configuration
### Installation & configuration
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.
#### Installation & configuration
### Installation & configuration
Install the package using `pip`.

View File

@ -78,7 +78,7 @@ Defaults to `False`
### `source`
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
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:
@ -159,14 +159,6 @@ Corresponds to `django.db.models.fields.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
@ -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)`
- `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.
- `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`.
* `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.
* `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`.
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')`
- `format`: Determines the representation format of the uuid value
- `'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"`
- `'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"`
* `format`: Determines the representation format of the uuid value
* `'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"`
* `'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"`
Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value`
## 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)`
- `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.
- `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_folders` - Specifies whether folders in the specified location should be included. Default is `False`. Either this or `allow_files` must be `True`.
* `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.
* `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_folders` - Specifies whether folders in the specified location should be included. Default is `False`. Either this or `allow_files` must be `True`.
## IPAddressField
@ -259,8 +251,8 @@ Corresponds to `django.forms.fields.IPAddressField` and `django.forms.fields.Gen
**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.
- `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'.
* `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'.
---
@ -274,8 +266,8 @@ Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.
**Signature**: `IntegerField(max_value=None, min_value=None)`
- `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.
* `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.
## FloatField
@ -285,8 +277,8 @@ Corresponds to `django.db.models.fields.FloatField`.
**Signature**: `FloatField(max_value=None, min_value=None)`
- `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.
* `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.
## 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)`
- `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.
- `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.
- `min_value` Validate that the number provided is no less than this value.
- `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
- `rounding` Sets the rounding mode used when quantising to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `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`.
* `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`.
* `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.
* `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`.
#### 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)
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
@ -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)`
- `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.
* `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.
---
@ -407,10 +395,10 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding
**Signature:** `ChoiceField(choices)`
- `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`.
- `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…"`
* `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`.
* `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…"`
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)`
- `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`.
- `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…"`
* `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`.
* `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…"`
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)`
- `max_length` - Designates the maximum length for the file name.
- `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.
* `max_length` - Designates the maximum length for the file name.
* `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.
## 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)`
- `max_length` - Designates the maximum length for the file name.
- `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.
* `max_length` - Designates the maximum length for the file name.
* `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.
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)`
- `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.
- `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.
* `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.
* `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.
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)`
- `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.
* `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.
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)`
- `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.
* `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.
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)`
- `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`.
* `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`.
---
@ -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)`
- `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:

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.
* `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`.
**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.
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
@ -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
filter = {}
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]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
@ -395,4 +395,4 @@ The following third party packages provide additional generic view implementatio
[UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels
[django-docs-select-related]: https://docs.djangoproject.com/en/3.1/ref/models/querysets/#django.db.models.query.QuerySet.select_related
[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
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.
* `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:
model = Album
fields = ['album_name', 'artist', 'tracks']
# For each album object, tracks should be fetched from database
qs = Album.objects.all()
print(AlbumSerializer(qs, many=True).data)
@ -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_
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.
@ -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:
- `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` - 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…"`
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/',
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
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.
For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`.
For `ModelSerializer` this defaults to `serializers.PrimaryKeyRelatedField`.
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.
##### 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:
@ -920,7 +920,7 @@ Or use it to serialize multiple instances:
serializer = HighScoreSerializer(queryset, many=True)
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.
@ -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 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):
"""
@ -1189,7 +1189,7 @@ The [drf-writable-nested][drf-writable-nested] package provides writable nested
## 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

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
python3 -m venv env
source env/bin/activate
pip install django
pip install -e .
pip install -r requirements.txt
# 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
* 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.
* 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.

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://findwork.dev/django-rest-framework-jobs][findwork-dev]
* [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.technojobs.co.uk/django-jobs][technobjobs-co-uk]
* [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/
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs
[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/
[technobjobs-co-uk]: https://www.technojobs.co.uk/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.1

View File

@ -17,7 +17,7 @@ By default, the API will return the format specified by the headers, which in th
## 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:
@ -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">
{% 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.
@ -44,7 +44,7 @@ Full example:
{% extends "rest_framework/base.html" %}
{% 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 %}
{% 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 = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>', views.snippet_detail),
path('snippets/<int:pk>/', views.snippet_detail),
]
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.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, basename="snippet")
router.register(r'users', views.UserViewSet, basename="user")
router.register(r'snippets', views.SnippetViewSet, basename='snippet')
router.register(r'users', views.UserViewSet, basename='user')
# The API URLs are now determined automatically by the router.
urlpatterns = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages.
"""
import django
from django.conf import settings
from django.views.generic import View
@ -152,6 +153,30 @@ else:
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
# See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':')

View File

@ -1,7 +1,7 @@
"""
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`)
"""
import math
@ -72,16 +72,19 @@ class ErrorDetail(str):
return self
def __eq__(self, other):
r = super().__eq__(other)
if r is NotImplemented:
result = super().__eq__(other)
if result is NotImplemented:
return NotImplemented
try:
return r and self.code == other.code
return result and self.code == other.code
except AttributeError:
return r
return result
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):
return 'ErrorDetail(string=%r, code=%r)' % (

View File

@ -1,3 +1,4 @@
import contextlib
import copy
import datetime
import decimal
@ -5,7 +6,6 @@ import functools
import inspect
import re
import uuid
import warnings
from collections import OrderedDict
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 pytz.exceptions import InvalidTimeError
from rest_framework import (
ISO_8601, RemovedInDRF313Warning, RemovedInDRF314Warning
)
from rest_framework import ISO_8601
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, json, representation
@ -265,16 +263,6 @@ class CreateOnlyDefault:
if is_update:
raise SkipField()
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):
return self.default(serializer_field)
else:
@ -504,16 +492,6 @@ class Field:
# No default, or this is a partial update.
raise SkipField()
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):
return self.default(self)
else:
@ -578,16 +556,6 @@ class Field:
"""
errors = []
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:
if getattr(validator, 'requires_context', False):
validator(value, self)
@ -723,15 +691,13 @@ class BooleanField(Field):
NULL_VALUES = {'null', 'Null', 'NULL', '', None}
def to_internal_value(self, data):
try:
with contextlib.suppress(TypeError):
if data in self.TRUE_VALUES:
return True
elif data in self.FALSE_VALUES:
return False
elif data in self.NULL_VALUES and self.allow_null:
return None
except TypeError: # Input is an unhashable type
pass
self.fail('invalid', input=data)
def to_representation(self, value):
@ -744,23 +710,6 @@ class BooleanField(Field):
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...
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 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 timezone.is_aware(value):
@ -1208,19 +1157,14 @@ class DateTimeField(Field):
return self.enforce_timezone(value)
for input_format in input_formats:
if input_format.lower() == ISO_8601:
try:
with contextlib.suppress(ValueError, TypeError):
if input_format.lower() == ISO_8601:
parsed = parse_datetime(value)
if parsed is not None:
return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
else:
try:
parsed = self.datetime_parser(value, input_format)
return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
parsed = self.datetime_parser(value, input_format)
return self.enforce_timezone(parsed)
humanized_format = humanize_datetime.datetime_formats(input_formats)
self.fail('invalid', format=humanized_format)
@ -1864,7 +1808,7 @@ class SerializerMethodField(Field):
For example:
class ExampleSerializer(self):
class ExampleSerializer(Serializer):
extra_info = SerializerMethodField()
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('--generator_class', dest="generator_class", 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):
if options['generator_class']:
@ -37,6 +38,7 @@ class Command(BaseCommand):
title=options['title'],
description=options['description'],
urlconf=options['urlconf'],
version=options['api_version'],
)
schema = generator.get_schema(request=None, public=True)
renderer = self.get_renderer(options['format'])

View File

@ -36,7 +36,6 @@ class SimpleMetadata(BaseMetadata):
label_lookup = ClassLookupDict({
serializers.Field: 'field',
serializers.BooleanField: 'boolean',
serializers.NullBooleanField: 'boolean',
serializers.CharField: 'string',
serializers.UUIDField: 'string',
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 rest_framework import HTTP_HEADER_ENCODING, exceptions
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import (
_MediaType, media_type_matches, order_by_precedence
@ -64,9 +64,11 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# Accepted media type is 'application/json'
full_media_type = ';'.join(
(renderer.media_type,) +
tuple('{}={}'.format(
key, value.decode(HTTP_HEADER_ENCODING))
for key, value in media_type_wrapper.params.items()))
tuple(
'{}={}'.format(key, value)
for key, value in media_type_wrapper.params.items()
)
)
return renderer, full_media_type
else:
# Eg client requests 'application/json; indent=8'

View File

@ -2,6 +2,8 @@
Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
import contextlib
from base64 import b64decode, b64encode
from collections import OrderedDict, namedtuple
from urllib import parse
@ -257,15 +259,12 @@ class PageNumberPagination(BasePagination):
def get_page_size(self, request):
if self.page_size_query_param:
try:
with contextlib.suppress(KeyError, ValueError):
return _positive_int(
request.query_params[self.page_size_query_param],
strict=True,
cutoff=self.max_page_size
)
except (KeyError, ValueError):
pass
return self.page_size
def get_next_link(self):
@ -430,15 +429,12 @@ class LimitOffsetPagination(BasePagination):
def get_limit(self, request):
if self.limit_query_param:
try:
with contextlib.suppress(KeyError, ValueError):
return _positive_int(
request.query_params[self.limit_query_param],
strict=True,
cutoff=self.max_limit
)
except (KeyError, ValueError):
pass
return self.default_limit
def get_offset(self, request):
@ -680,15 +676,12 @@ class CursorPagination(BasePagination):
def get_page_size(self, request):
if self.page_size_query_param:
try:
with contextlib.suppress(KeyError, ValueError):
return _positive_int(
request.query_params[self.page_size_query_param],
strict=True,
cutoff=self.max_page_size
)
except (KeyError, ValueError):
pass
return self.page_size
def get_next_link(self):
@ -905,10 +898,16 @@ class CursorPagination(BasePagination):
'next': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{cursor_query_param}=cD00ODY%3D"'.format(
cursor_query_param=self.cursor_query_param)
},
'previous': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{cursor_query_param}=cj0xJnA9NDg3'.format(
cursor_query_param=self.cursor_query_param)
},
'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
on the request, such as form content or json encoded data.
"""
import codecs
from urllib import parse
import contextlib
from django.conf import settings
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 \
MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header
from django.utils.encoding import force_str
from django.http.multipartparser import MultiPartParserError
from rest_framework import renderers
from rest_framework.compat import parse_header_parameters
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.utils import json
@ -194,30 +195,12 @@ class FileUploadParser(BaseParser):
Detects the uploaded file name. First searches a 'filename' url kwarg.
Then tries to parse Content-Disposition header.
"""
try:
with contextlib.suppress(KeyError):
return parser_context['kwargs']['filename']
except KeyError:
pass
try:
with contextlib.suppress(AttributeError, KeyError, ValueError):
meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode())
filename_parm = disposition[1]
if 'filename*' in filename_parm:
return self.get_encoded_filename(filename_parm)
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
disposition, params = parse_header_parameters(meta['HTTP_CONTENT_DISPOSITION'])
if 'filename*' in params:
return params['filename*']
return params['filename']

View File

@ -46,6 +46,14 @@ class OperandHolder(OperationHolderMixin):
op2 = self.op2_class(*args, **kwargs)
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:
def __init__(self, op1, op2):
@ -78,8 +86,11 @@ class OR:
def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) or
self.op2.has_object_permission(request, view, obj)
self.op1.has_permission(request, view)
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
from collections import OrderedDict
from urllib import parse
@ -170,7 +171,7 @@ class RelatedField(Field):
def get_attribute(self, instance):
if self.use_pk_only_optimization() and self.source_attrs:
# 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])
value = attribute_instance.serializable_value(self.source_attrs[-1])
if is_simple_callable(value):
@ -183,9 +184,6 @@ class RelatedField(Field):
value = getattr(value, 'pk', value)
return PKOnlyObject(pk=value)
except AttributeError:
pass
# Standard case, return the object 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.
"""
import base64
import contextlib
from collections import OrderedDict
from urllib import parse
@ -14,7 +16,6 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import engines, loader
from django.urls import NoReverseMatch
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.compat import (
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.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',
# then pretty print the result.
# Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
try:
base_media_type, params = parse_header_parameters(accepted_media_type)
with contextlib.suppress(KeyError, ValueError, TypeError):
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.
# E.g. If we're being called by the BrowsableAPIRenderer.
return renderer_context.get('indent', None)
@ -489,11 +487,8 @@ class BrowsableAPIRenderer(BaseRenderer):
return
if existing_serializer is not None:
try:
with contextlib.suppress(TypeError):
return self.render_form_for_serializer(existing_serializer)
except TypeError:
pass
if has_serializer:
if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, **kwargs)

View File

@ -14,11 +14,11 @@ from contextlib import contextmanager
from django.conf import settings
from django.http import HttpRequest, QueryDict
from django.http.multipartparser import parse_header
from django.http.request import RawPostDataException
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
@ -26,7 +26,7 @@ def is_form_media_type(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
base_media_type == 'multipart/form-data')
@ -413,7 +413,8 @@ class Request:
to proxy it to the underlying HttpRequest object.
"""
try:
return getattr(self._request, attr)
_request = self.__getattribute__("_request")
return getattr(_request, attr)
except AttributeError:
return self.__getattribute__(attr)

View File

@ -198,7 +198,11 @@ class SchemaGenerator(BaseSchemaGenerator):
if is_custom_action(action):
# 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()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]

View File

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

View File

@ -13,7 +13,7 @@ from django.db import models
from django.utils.encoding import force_str
from rest_framework import (
RemovedInDRF314Warning, exceptions, renderers, serializers
RemovedInDRF315Warning, exceptions, renderers, serializers
)
from rest_framework.compat import uritemplate
from rest_framework.fields import _UnvalidatedField, empty
@ -523,7 +523,7 @@ class AutoSchema(ViewInspector):
continue
if field.required:
required.append(field.field_name)
required.append(self.get_field_name(field))
schema = self.map_field(field)
if field.read_only:
@ -538,7 +538,7 @@ class AutoSchema(ViewInspector):
schema['description'] = str(field.help_text)
self.map_field_validators(field, schema)
properties[field.field_name] = schema
properties[self.get_field_name(field)] = schema
result = {
'type': 'object',
@ -589,6 +589,13 @@ class AutoSchema(ViewInspector):
schema['maximum'] = int(digits * '9') + 1
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):
pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class:
@ -713,106 +720,10 @@ class AutoSchema(ViewInspector):
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):
warnings.warn(
"Method `_get_reference()` has been renamed to `get_reference()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
"The old name will be removed in DRF v3.15.",
RemovedInDRF315Warning, stacklevel=2
)
return self.get_reference(serializer)

View File

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

View File

@ -19,7 +19,9 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
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 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) {
params[paramKey] = value
}
} else if (dataType === 'array' && paramValue) {
try {
params[paramKey] = JSON.parse(paramValue)
} catch (err) {
// Ignore malformed JSON
}
} else if (dataType === 'object' && paramValue) {
} else if ((dataType === 'array' && paramValue) || (dataType === 'object' && paramValue)) {
try {
params[paramKey] = JSON.parse(paramValue)
} catch (err) {

View File

@ -277,7 +277,7 @@ class APIClient(APIRequestFactory, DjangoClient):
"""
self.handler._force_user = user
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
def request(self, **kwargs):

View File

@ -171,7 +171,7 @@ class AnonRateThrottle(SimpleRateThrottle):
scope = 'anon'
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 self.cache_format % {
@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):
scope = 'user'
def get_cache_key(self, request, view):
if request.user.is_authenticated:
if request.user and request.user.is_authenticated:
ident = request.user.pk
else:
ident = self.get_ident(request)
@ -237,9 +237,9 @@ class ScopedRateThrottle(SimpleRateThrottle):
If `view.throttle_scope` is not set, don't apply this throttle.
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
else:
ident = self.get_ident(request)

View File

@ -1,6 +1,8 @@
"""
Helper classes for parsers.
"""
import contextlib
import datetime
import decimal
import json # noqa
@ -58,10 +60,8 @@ class JSONEncoder(json.JSONEncoder):
)
elif hasattr(obj, '__getitem__'):
cls = (list if isinstance(obj, (list, tuple)) else dict)
try:
with contextlib.suppress(Exception):
return cls(obj)
except Exception:
pass
elif hasattr(obj, '__iter__'):
return tuple(item for item in 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)):
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 this field is read-only, then return early.
# 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:
kwargs['required'] = False
if model_field.null:
kwargs['allow_null'] = True
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
kwargs['allow_blank'] = True
@ -263,6 +263,8 @@ def get_relation_kwargs(field_name, relation_info):
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if kwargs.get('read_only', False):
# If this field is read-only, then return early.
# 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:
kwargs['required'] = False
if model_field.null:
kwargs['allow_null'] = True
if model_field.validators:
kwargs['validators'] = model_field.validators
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
"""
from django.http.multipartparser import parse_header
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.compat import parse_header_parameters
def media_type_matches(lhs, rhs):
@ -46,7 +44,7 @@ def order_by_precedence(media_type_lst):
class _MediaType:
def __init__(self, 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('/')
def match(self, other):
@ -79,5 +77,5 @@ class _MediaType:
def __str__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items():
ret += "; %s=%s" % (key, val.decode('ascii'))
ret += "; %s=%s" % (key, val)
return ret

View File

@ -1,3 +1,4 @@
import contextlib
import sys
from collections import OrderedDict
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
# value will be a JSONString, rather than a JSON primitive.
if not getattr(value, 'is_json_string', False):
try:
with contextlib.suppress(TypeError, ValueError):
value = json.dumps(
self.value,
sort_keys=True,
indent=4,
separators=(',', ': '),
)
except (TypeError, ValueError):
pass
return self.__class__(self._field, value, self.errors, self._prefix)

View File

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

View File

@ -198,6 +198,10 @@ class ViewSetMixin:
for action in actions:
try:
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)
view = self.__class__(**action.kwargs)
action_urls[view.get_view_name()] = url

View File

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

View File

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

View File

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

View File

@ -754,6 +754,67 @@ class TestSchemaGeneratorWithManyToMany(TestCase):
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')
@override_settings(REST_FRAMEWORK={'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'})
class Test4605Regression(TestCase):

View File

@ -52,9 +52,9 @@ class GenerateSchemaTests(TestCase):
@pytest.mark.skipif(yaml is None, reason='PyYAML is required.')
def test_renders_default_schema_with_custom_title_url_and_description(self):
call_command('generateschema',
'--title=SampleAPI',
'--url=http://api.sample.com',
'--description=Sample description',
'--title=ExampleAPI',
'--url=http://api.example.com',
'--description=Example description',
stdout=self.out)
# Check valid YAML was output.
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'})
def test_coreapi_renders_default_schema_with_custom_title_url_and_description(self):
expected_out = """info:
description: Sample description
title: SampleAPI
description: Example description
title: ExampleAPI
version: ''
openapi: 3.0.0
paths:
@ -103,12 +103,12 @@ class GenerateSchemaTests(TestCase):
get:
operationId: list
servers:
- url: http://api.sample.com/
- url: http://api.example.com/
"""
call_command('generateschema',
'--title=SampleAPI',
'--url=http://api.sample.com',
'--description=Sample description',
'--title=ExampleAPI',
'--url=http://api.example.com',
'--description=Example description',
stdout=self.out)
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 '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):
class Model(models.Model):
rw_field = models.CharField(null=True)

View File

@ -1,5 +1,3 @@
import sys
import pytest
from django.test import TestCase
@ -33,7 +31,7 @@ indented
# If markdown is installed we also test it's working
# (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>
<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
</code></pre>
<p>indented</p>
<h2 id="hash-style-header">hash style header</h2>%s"""
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>
<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>
<p><br /></p>"""
@ -163,11 +144,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
Ensure markdown to HTML works as expected.
"""
# Markdown 3.3 is only supported on Python 3.6 and higher
if sys.version_info >= (3, 6):
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33
else:
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33
assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
def test_dedent_tabs():

View File

@ -566,7 +566,7 @@ class TestCreateOnlyDefault:
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
"""
class TestCallableDefault:
@ -679,9 +679,9 @@ class TestBooleanField(FieldValues):
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 = {
'true': True,
@ -706,16 +706,6 @@ class TestNullBooleanField(TestBooleanField):
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...
class TestCharField(FieldValues):

View File

@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from django.test import TestCase
from rest_framework import generics, renderers, serializers, status
from rest_framework.exceptions import ErrorDetail
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from tests.models import (
@ -519,7 +520,12 @@ class TestFilterBackendAppliedToViews(TestCase):
request = factory.get('/1')
response = instance_view(request, pk=1).render()
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):
"""

View File

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

View File

@ -635,7 +635,7 @@ class PermissionsCompositionTests(TestCase):
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None)
assert hasperm is True
assert mock_deny.call_count == 1
assert mock_deny.call_count == 0
assert mock_allow.call_count == 1
def test_and_lazyness(self):
@ -677,3 +677,16 @@ class PermissionsCompositionTests(TestCase):
assert hasperm is False
assert mock_deny.call_count == 1
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.
"""
import copy
import os.path
import tempfile
@ -344,3 +345,10 @@ class TestHttpRequest(TestCase):
# ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data')
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 rest_framework import fields, serializers
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.test import (
@ -19,10 +20,12 @@ from rest_framework.test import (
@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
def view(request):
return Response({
'auth': request.META.get('HTTP_AUTHORIZATION', b''),
'user': request.user.username
})
data = {'auth': request.META.get('HTTP_AUTHORIZATION', b'')}
if request.user:
data['user'] = request.user.username
if request.auth:
data['token'] = request.auth.key
return Response(data)
@api_view(['GET', 'POST'])
@ -78,14 +81,46 @@ class TestAPITestClient(TestCase):
response = self.client.get('/view/')
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')
self.client.force_authenticate(user)
self.client.force_authenticate(user=user)
response = self.client.get('/view/')
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):
"""
@ -102,8 +137,9 @@ class TestAPITestClient(TestCase):
response = self.client.get('/session-view/')
assert response.data['active_session'] is True
# Force authenticating as `None` should also logout the user session.
self.client.force_authenticate(None)
# Force authenticating with `None` user and token should also logout
# the user session.
self.client.force_authenticate(user=None, token=None)
response = self.client.get('/session-view/')
assert response.data['active_session'] is False

View File

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