Merge branch 'master' into sanitize-searchfield-input
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
custom: https://fund.django-rest-framework.org/topics/funding/
|
|
@ -4,10 +4,6 @@ dist: xenial
|
|||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- { python: "2.7", env: DJANGO=1.11 }
|
||||
|
||||
- { python: "3.4", env: DJANGO=1.11 }
|
||||
- { python: "3.4", env: DJANGO=2.0 }
|
||||
|
||||
- { python: "3.5", env: DJANGO=1.11 }
|
||||
- { python: "3.5", env: DJANGO=2.0 }
|
||||
|
@ -26,8 +22,8 @@ matrix:
|
|||
- { python: "3.7", env: DJANGO=master }
|
||||
|
||||
- { python: "3.7", env: TOXENV=base }
|
||||
- { python: "2.7", env: TOXENV=lint }
|
||||
- { python: "2.7", env: TOXENV=docs }
|
||||
- { python: "3.7", env: TOXENV=lint }
|
||||
- { python: "3.7", env: TOXENV=docs }
|
||||
|
||||
- python: "3.7"
|
||||
env: TOXENV=dist
|
||||
|
|
1
CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
docs/community/release-notes.md
|
|
@ -59,7 +59,7 @@ Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recom
|
|||
To run the tests, clone the repository, and then:
|
||||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install django
|
||||
pip install -r requirements.txt
|
||||
|
@ -115,7 +115,7 @@ It's also useful to remember that if you have an outstanding pull request then p
|
|||
|
||||
GitHub's documentation for working on pull requests is [available here][pull-requests].
|
||||
|
||||
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
|
||||
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django.
|
||||
|
||||
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
|
||||
|
||||
|
|
18
README.md
|
@ -24,10 +24,10 @@ The initial aim is to provide a single full-time position on REST framework.
|
|||
[![][rollbar-img]][rollbar-url]
|
||||
[![][cadre-img]][cadre-url]
|
||||
[![][kloudless-img]][kloudless-url]
|
||||
[![][release-history-img]][release-history-url]
|
||||
[![][esg-img]][esg-url]
|
||||
[![][lightson-img]][lightson-url]
|
||||
|
||||
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url].
|
||||
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [ESG][esg-url], and [Lights On Software][lightson-url].
|
||||
|
||||
---
|
||||
|
||||
|
@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
|
||||
# Requirements
|
||||
|
||||
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||
* Python (3.5, 3.6, 3.7)
|
||||
* Django (1.11, 2.0, 2.1, 2.2)
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
|
@ -175,9 +175,7 @@ You may also want to [follow the author on Twitter][twitter].
|
|||
|
||||
# Security
|
||||
|
||||
If you believe you've found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||
|
||||
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||
Please see the [security policy][security-policy].
|
||||
|
||||
[build-status-image]: https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master
|
||||
[travis]: https://travis-ci.org/encode/django-rest-framework?branch=master
|
||||
|
@ -199,17 +197,15 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
|
||||
[load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png
|
||||
[kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png
|
||||
[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history.png
|
||||
[esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png
|
||||
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
|
||||
|
||||
[rover-url]: http://jobs.rover.com/
|
||||
[sentry-url]: https://getsentry.com/welcome/
|
||||
[stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf
|
||||
[rollbar-url]: https://rollbar.com/
|
||||
[cadre-url]: https://cadre.com/
|
||||
[load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf
|
||||
[kloudless-url]: https://hubs.ly/H0f30Lf0
|
||||
[release-history-url]: https://releasehistory.io
|
||||
[esg-url]: https://software.esg-usa.com/
|
||||
[lightson-url]: https://lightsonsoftware.com
|
||||
|
||||
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
|
||||
|
@ -225,4 +221,4 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[image]: https://www.django-rest-framework.org/img/quickstart.png
|
||||
|
||||
[docs]: https://www.django-rest-framework.org/
|
||||
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
||||
[security-policy]: https://github.com/encode/django-rest-framework/security/policy
|
||||
|
|
9
SECURITY.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you've found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||
|
||||
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||
|
||||
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
|
@ -1,4 +1,7 @@
|
|||
source: authentication.py
|
||||
---
|
||||
source:
|
||||
- authentication.py
|
||||
---
|
||||
|
||||
# Authentication
|
||||
|
||||
|
@ -327,7 +330,7 @@ If the `.authenticate_header()` method is not overridden, the authentication sch
|
|||
|
||||
## Example
|
||||
|
||||
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
|
||||
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X-USERNAME'.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import authentication
|
||||
|
@ -335,7 +338,7 @@ The following example will authenticate any incoming request as the user given b
|
|||
|
||||
class ExampleAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
username = request.META.get('X_USERNAME')
|
||||
username = request.META.get('HTTP_X_USERNAME')
|
||||
if not username:
|
||||
return None
|
||||
|
||||
|
@ -354,7 +357,7 @@ The following third party packages are also available.
|
|||
|
||||
## Django OAuth Toolkit
|
||||
|
||||
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
|
||||
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
|
||||
|
||||
#### Installation & configuration
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Caching
|
||||
|
||||
> A certain woman had a very sharp conciousness but almost no
|
||||
> A certain woman had a very sharp consciousness but almost no
|
||||
> memory ... She remembered enough to work, and she worked hard.
|
||||
> - Lydia Davis
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: negotiation.py
|
||||
---
|
||||
source:
|
||||
- negotiation.py
|
||||
---
|
||||
|
||||
# Content negotiation
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: exceptions.py
|
||||
---
|
||||
source:
|
||||
- exceptions.py
|
||||
---
|
||||
|
||||
# Exceptions
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: fields.py
|
||||
---
|
||||
source:
|
||||
- fields.py
|
||||
---
|
||||
|
||||
# Serializer fields
|
||||
|
||||
|
@ -209,7 +212,7 @@ 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 cannoncical 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"`
|
||||
- `'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"`
|
||||
|
@ -448,9 +451,10 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
|
|||
|
||||
A field class that validates a list of objects.
|
||||
|
||||
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, 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.
|
||||
- `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.
|
||||
|
||||
|
@ -471,9 +475,10 @@ We can now reuse our custom `StringListField` class throughout our application,
|
|||
|
||||
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
|
||||
|
||||
**Signature**: `DictField(child=<A_FIELD_INSTANCE>)`
|
||||
**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.
|
||||
|
||||
For example, to create a field that validates a mapping of strings to strings, you would write something like this:
|
||||
|
||||
|
@ -488,9 +493,10 @@ You can also use the declarative style, as with `ListField`. For example:
|
|||
|
||||
A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.
|
||||
|
||||
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>)`
|
||||
**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.
|
||||
|
||||
Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.
|
||||
|
||||
|
@ -498,9 +504,10 @@ Note that the child field **must** be an instance of `CharField`, as the hstore
|
|||
|
||||
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.
|
||||
|
||||
**Signature**: `JSONField(binary)`
|
||||
**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`.
|
||||
|
||||
---
|
||||
|
||||
|
@ -629,7 +636,7 @@ Our `ColorField` class above currently does not perform any data validation.
|
|||
To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, six.text_type):
|
||||
if not isinstance(data, str):
|
||||
msg = 'Incorrect type. Expected a string, but got %s'
|
||||
raise ValidationError(msg % type(data).__name__)
|
||||
|
||||
|
@ -653,7 +660,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me
|
|||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, six.text_type):
|
||||
if not isinstance(data, str):
|
||||
self.fail('incorrect_type', input_type=type(data).__name__)
|
||||
|
||||
if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: filters.py
|
||||
---
|
||||
source:
|
||||
- filters.py
|
||||
---
|
||||
|
||||
# Filtering
|
||||
|
||||
|
@ -221,7 +224,7 @@ By default, the search parameter is named `'search`', but this may be overridden
|
|||
To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request:
|
||||
|
||||
from rest_framework import filters
|
||||
|
||||
|
||||
class CustomSearchFilter(filters.SearchFilter):
|
||||
def get_search_fields(self, view, request):
|
||||
if request.query_params.get('title_only'):
|
||||
|
@ -291,53 +294,6 @@ The `ordering` attribute may be either a string or a list/tuple of strings.
|
|||
|
||||
---
|
||||
|
||||
## DjangoObjectPermissionsFilter
|
||||
|
||||
The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission.
|
||||
|
||||
---
|
||||
|
||||
**Note:** This filter has been deprecated as of version 3.9 and moved to the 3rd-party [`djangorestframework-guardian` package][django-rest-framework-guardian].
|
||||
|
||||
---
|
||||
|
||||
If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute.
|
||||
|
||||
A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this.
|
||||
|
||||
**permissions.py**:
|
||||
|
||||
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
|
||||
"""
|
||||
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
|
||||
"""
|
||||
perms_map = {
|
||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
||||
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
|
||||
'HEAD': ['%(app_label)s.view_%(model_name)s'],
|
||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||
}
|
||||
|
||||
**views.py**:
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Viewset that only lists events if user has 'view' permissions, and only
|
||||
allows operations on individual events if user has appropriate 'view', 'add',
|
||||
'change' or 'delete' permissions.
|
||||
"""
|
||||
queryset = Event.objects.all()
|
||||
serializer_class = EventSerializer
|
||||
filter_backends = (filters.DjangoObjectPermissionsFilter,)
|
||||
permission_classes = (myapp.permissions.CustomObjectPermissions,)
|
||||
|
||||
For more information on adding `'view'` permissions for models, see the [relevant section][view-permissions] of the `django-guardian` documentation, and [this blogpost][view-permissions-blogpost].
|
||||
|
||||
---
|
||||
|
||||
# Custom generic filtering
|
||||
|
||||
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
|
||||
|
@ -399,12 +355,8 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
|
|||
[cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||
[django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html
|
||||
[django-filter-drf-docs]: https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html
|
||||
[guardian]: https://django-guardian.readthedocs.io/
|
||||
[view-permissions]: https://django-guardian.readthedocs.io/en/latest/userguide/assign.html
|
||||
[view-permissions-blogpost]: https://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
||||
[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
||||
[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
|
||||
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
|
||||
[django-url-filter]: https://github.com/miki725/django-url-filter
|
||||
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: urlpatterns.py
|
||||
---
|
||||
source:
|
||||
- urlpatterns.py
|
||||
---
|
||||
|
||||
# Format suffixes
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
source: mixins.py
|
||||
generics.py
|
||||
---
|
||||
source:
|
||||
- mixins.py
|
||||
- generics.py
|
||||
---
|
||||
|
||||
# Generic views
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: metadata.py
|
||||
---
|
||||
source:
|
||||
- metadata.py
|
||||
---
|
||||
|
||||
# Metadata
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: parsers.py
|
||||
---
|
||||
source:
|
||||
- parsers.py
|
||||
---
|
||||
|
||||
# Parsers
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: permissions.py
|
||||
---
|
||||
source:
|
||||
- permissions.py
|
||||
---
|
||||
|
||||
# Permissions
|
||||
|
||||
|
@ -281,6 +284,10 @@ Also note that the generic views will only check the object-level permissions fo
|
|||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF - Access Policy
|
||||
|
||||
The [Django REST - Access Policy][drf-access-policy] package provides a way to define complex access rules in declaritive policy classes that are attached to view sets or function-based views. The policies are defined in JSON in a format similar to AWS' Identity & Access Management policies.
|
||||
|
||||
## Composed Permissions
|
||||
|
||||
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
|
||||
|
@ -299,7 +306,7 @@ The [Django Rest Framework Roles][django-rest-framework-roles] package makes it
|
|||
|
||||
## Django REST Framework API Key
|
||||
|
||||
The [Django REST Framework API Key][djangorestframework-api-key] package provides the ability to authorize clients based on customizable API key headers. This package is targeted at situations in which regular user-based authentication (e.g. `TokenAuthentication`) is not suitable, e.g. allowing non-human clients to safely use your API. API keys are generated and validated through cryptographic methods and can be created and revoked from the Django admin interface at anytime.
|
||||
The [Django REST Framework API Key][djangorestframework-api-key] package provides permissions classes, models and helpers to add API key authorization to your API. It can be used to authorize internal or third-party backends and services (i.e. _machines_) which do not have a user account. API keys are stored securely using Django's password hashing infrastructure, and they can be viewed, edited and revoked at anytime in the Django admin.
|
||||
|
||||
## Django Rest Framework Role Filters
|
||||
|
||||
|
@ -317,6 +324,7 @@ The [Django Rest Framework Role Filters][django-rest-framework-role-filters] pac
|
|||
[rest-condition]: https://github.com/caxap/rest_condition
|
||||
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
||||
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
||||
[djangorestframework-api-key]: https://github.com/florimondmanca/djangorestframework-api-key
|
||||
[djangorestframework-api-key]: https://florimondmanca.github.io/djangorestframework-api-key/
|
||||
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
|
||||
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||
[drf-access-policy]: https://github.com/rsinger86/drf-access-policy
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: relations.py
|
||||
---
|
||||
source:
|
||||
- relations.py
|
||||
---
|
||||
|
||||
# Serializer relations
|
||||
|
||||
|
@ -576,6 +579,8 @@ If you explicitly specify a relational field pointing to a
|
|||
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||
to ``True``.
|
||||
|
||||
If you wish to represent [extra fields on a through model][django-intermediary-manytomany] then you may serialize the through model as [a nested object][dealing-with-nested-objects].
|
||||
|
||||
---
|
||||
|
||||
# Third Party Packages
|
||||
|
@ -596,3 +601,5 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re
|
|||
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations
|
||||
[django-intermediary-manytomany]: https://docs.djangoproject.com/en/2.2/topics/db/models/#intermediary-manytomany
|
||||
[dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: renderers.py
|
||||
---
|
||||
source:
|
||||
- renderers.py
|
||||
---
|
||||
|
||||
# Renderers
|
||||
|
||||
|
@ -534,7 +537,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
[messagepack]: https://msgpack.org/
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[mjumbewu]: https://github.com/mjumbewu
|
||||
[flipperpa]: https://githuc.com/flipperpa
|
||||
[flipperpa]: https://github.com/flipperpa
|
||||
[wharton]: https://github.com/wharton
|
||||
[drf-renderer-xlsx]: https://github.com/wharton/drf-renderer-xlsx
|
||||
[vbabiy]: https://github.com/vbabiy
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: request.py
|
||||
---
|
||||
source:
|
||||
- request.py
|
||||
---
|
||||
|
||||
# Requests
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: response.py
|
||||
---
|
||||
source:
|
||||
- response.py
|
||||
---
|
||||
|
||||
# Responses
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: reverse.py
|
||||
---
|
||||
source:
|
||||
- reverse.py
|
||||
---
|
||||
|
||||
# Returning URLs
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: routers.py
|
||||
---
|
||||
source:
|
||||
- routers.py
|
||||
---
|
||||
|
||||
# Routers
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: schemas.py
|
||||
---
|
||||
source:
|
||||
- schemas.py
|
||||
---
|
||||
|
||||
# Schemas
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: serializers.py
|
||||
---
|
||||
source:
|
||||
- serializers.py
|
||||
---
|
||||
|
||||
# Serializers
|
||||
|
||||
|
@ -572,6 +575,8 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu
|
|||
user.save()
|
||||
return user
|
||||
|
||||
Please keep in mind that, if the field has already been explicitly declared on the serializer class, then the `extra_kwargs` option will be ignored.
|
||||
|
||||
## Relational fields
|
||||
|
||||
When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances.
|
||||
|
@ -624,7 +629,7 @@ The default implementation returns a serializer class based on the `serializer_f
|
|||
|
||||
Called to generate a serializer field that maps to a relational model field.
|
||||
|
||||
The default implementation returns a serializer class based on the `serializer_relational_field` attribute.
|
||||
The default implementation returns a serializer class based on the `serializer_related_field` attribute.
|
||||
|
||||
The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
|
||||
|
||||
|
@ -963,7 +968,7 @@ The following class is an example of a generic serializer that can handle coerci
|
|||
def to_representation(self, obj):
|
||||
for attribute_name in dir(obj):
|
||||
attribute = getattr(obj, attribute_name)
|
||||
if attribute_name('_'):
|
||||
if attribute_name.startswith('_'):
|
||||
# Ignore private attributes.
|
||||
pass
|
||||
elif hasattr(attribute, '__call__'):
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: settings.py
|
||||
---
|
||||
source:
|
||||
- settings.py
|
||||
---
|
||||
|
||||
# Settings
|
||||
|
||||
|
@ -404,7 +407,7 @@ This should be a function with the following signature:
|
|||
|
||||
If the view instance inherits `ViewSet`, it may have been initialized with several optional arguments:
|
||||
|
||||
* `name`: A name expliticly provided to a view in the viewset. Typically, this value should be used as-is when provided.
|
||||
* `name`: A name explicitly provided to a view in the viewset. Typically, this value should be used as-is when provided.
|
||||
* `suffix`: Text used when differentiating individual views in a viewset. This argument is mutually exclusive to `name`.
|
||||
* `detail`: Boolean that differentiates an individual view in a viewset as either being a 'list' or 'detail' view.
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: status.py
|
||||
---
|
||||
source:
|
||||
- status.py
|
||||
---
|
||||
|
||||
# Status Codes
|
||||
|
||||
|
@ -20,13 +23,13 @@ The full set of HTTP status codes included in the `status` module is listed belo
|
|||
The module also includes a set of helper functions for testing if a status code is in a given range.
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
class ExampleTestCase(APITestCase):
|
||||
def test_url_root(self):
|
||||
url = reverse('index')
|
||||
response = self.client.get(url)
|
||||
self.assertTrue(status.is_success(response.status_code))
|
||||
class ExampleTestCase(APITestCase):
|
||||
def test_url_root(self):
|
||||
url = reverse('index')
|
||||
response = self.client.get(url)
|
||||
self.assertTrue(status.is_success(response.status_code))
|
||||
|
||||
|
||||
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
|
||||
|
@ -51,6 +54,8 @@ This class of status code indicates that the client's request was successfully r
|
|||
HTTP_205_RESET_CONTENT
|
||||
HTTP_206_PARTIAL_CONTENT
|
||||
HTTP_207_MULTI_STATUS
|
||||
HTTP_208_ALREADY_REPORTED
|
||||
HTTP_226_IM_USED
|
||||
|
||||
## Redirection - 3xx
|
||||
|
||||
|
@ -64,6 +69,7 @@ This class of status code indicates that further action needs to be taken by the
|
|||
HTTP_305_USE_PROXY
|
||||
HTTP_306_RESERVED
|
||||
HTTP_307_TEMPORARY_REDIRECT
|
||||
HTTP_308_PERMANENT_REDIRECT
|
||||
|
||||
## Client Error - 4xx
|
||||
|
||||
|
@ -90,6 +96,7 @@ The 4xx class of status code is intended for cases in which the client seems to
|
|||
HTTP_422_UNPROCESSABLE_ENTITY
|
||||
HTTP_423_LOCKED
|
||||
HTTP_424_FAILED_DEPENDENCY
|
||||
HTTP_426_UPGRADE_REQUIRED
|
||||
HTTP_428_PRECONDITION_REQUIRED
|
||||
HTTP_429_TOO_MANY_REQUESTS
|
||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE
|
||||
|
@ -105,7 +112,11 @@ Response status codes beginning with the digit "5" indicate cases in which the s
|
|||
HTTP_503_SERVICE_UNAVAILABLE
|
||||
HTTP_504_GATEWAY_TIMEOUT
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
|
||||
HTTP_506_VARIANT_ALSO_NEGOTIATES
|
||||
HTTP_507_INSUFFICIENT_STORAGE
|
||||
HTTP_508_LOOP_DETECTED
|
||||
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED
|
||||
HTTP_510_NOT_EXTENDED
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
|
||||
|
||||
## Helper functions
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: test.py
|
||||
---
|
||||
source:
|
||||
- test.py
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: throttling.py
|
||||
---
|
||||
source:
|
||||
- throttling.py
|
||||
---
|
||||
|
||||
# Throttling
|
||||
|
||||
|
@ -74,7 +77,7 @@ If you need to strictly identify unique client IP addresses, you'll need to firs
|
|||
|
||||
It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](https://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client.
|
||||
|
||||
Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifing-clients].
|
||||
Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifying-clients].
|
||||
|
||||
## Setting up the cache
|
||||
|
||||
|
@ -194,6 +197,6 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
|
|||
|
||||
[cite]: https://developer.twitter.com/en/docs/basics/rate-limiting
|
||||
[permissions]: permissions.md
|
||||
[identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
|
||||
[identifying-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
|
||||
[cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches
|
||||
[cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: validators.py
|
||||
---
|
||||
source:
|
||||
- validators.py
|
||||
---
|
||||
|
||||
# Validators
|
||||
|
||||
|
@ -100,7 +103,7 @@ The validator should be applied to *serializer classes*, like so:
|
|||
|
||||
---
|
||||
|
||||
**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
**Note**: The `UniqueTogetherValidator` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
|
||||
---
|
||||
|
||||
|
@ -159,7 +162,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
|
|||
|
||||
---
|
||||
|
||||
**Note**: The `UniqueFor<Range>Validation` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
**Note**: The `UniqueFor<Range>Validator` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: versioning.py
|
||||
---
|
||||
source:
|
||||
- versioning.py
|
||||
---
|
||||
|
||||
# Versioning
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
source: decorators.py
|
||||
views.py
|
||||
---
|
||||
source:
|
||||
- decorators.py
|
||||
- views.py
|
||||
---
|
||||
|
||||
# Class-based Views
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
source: viewsets.py
|
||||
---
|
||||
source:
|
||||
- viewsets.py
|
||||
---
|
||||
|
||||
# ViewSets
|
||||
|
||||
|
|
|
@ -523,7 +523,7 @@ The following class is an example of a generic serializer that can handle coerci
|
|||
def to_representation(self, obj):
|
||||
for attribute_name in dir(obj):
|
||||
attribute = getattr(obj, attribute_name)
|
||||
if attribute_name('_'):
|
||||
if attribute_name.startswith('_'):
|
||||
# Ignore private attributes.
|
||||
pass
|
||||
elif hasattr(attribute, '__call__'):
|
||||
|
|
|
@ -60,7 +60,7 @@ REST framework's new API documentation supports a number of features:
|
|||
* Support for various authentication schemes.
|
||||
* Code snippets for the Python, JavaScript, and Command Line clients.
|
||||
|
||||
The `coreapi` library is required as a dependancy for the API docs. Make sure
|
||||
The `coreapi` library is required as a dependency for the API docs. Make sure
|
||||
to install the latest version (2.3.0 or above). The `pygments` and `markdown`
|
||||
libraries are optional but recommended.
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recom
|
|||
To run the tests, clone the repository, and then:
|
||||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install django
|
||||
pip install -r requirements.txt
|
||||
|
@ -121,7 +121,7 @@ It's also useful to remember that if you have an outstanding pull request then p
|
|||
|
||||
GitHub's documentation for working on pull requests is [available here][pull-requests].
|
||||
|
||||
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
|
||||
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django.
|
||||
|
||||
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
|
|||
* [https://www.python.org/jobs/][python-org-jobs]
|
||||
* [https://djangogigs.com][django-gigs-com]
|
||||
* [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://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com]
|
||||
|
@ -26,6 +27,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
|
|||
[python-org-jobs]: https://www.python.org/jobs/
|
||||
[django-gigs-com]: https://djangogigs.com
|
||||
[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
|
||||
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/
|
||||
|
|
|
@ -38,11 +38,38 @@ You can determine your currently installed version using `pip show`:
|
|||
|
||||
---
|
||||
|
||||
## 3.10.x series
|
||||
|
||||
### 3.10.0
|
||||
|
||||
**Date**: [Unreleased][3.10.0-milestone]
|
||||
|
||||
* Updated PyYaml dependency for OpenAPI schema generation to `pyyaml>=5.1` [#6680][gh6680]
|
||||
* Resolve DeprecationWarning with markdown. [#6317][gh6317]
|
||||
* Add `generateschema --generator_class` CLI option
|
||||
|
||||
|
||||
## 3.9.x series
|
||||
|
||||
### 3.9.4
|
||||
|
||||
**Date**: 10th May 2019
|
||||
|
||||
This is a maintenance release that fixes an error handling bug under Python 2.
|
||||
|
||||
### 3.9.3
|
||||
|
||||
**Date**: 29th April 2019
|
||||
|
||||
This is the last Django REST Framework release that will support Python 2.
|
||||
Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10.
|
||||
|
||||
* Adjusted the compat check for django-guardian to allow the last guardian
|
||||
version (v1.4.9) compatible with Python 2. [#6613][gh6613]
|
||||
|
||||
### 3.9.2
|
||||
|
||||
**Date**: [3rd March 2019][3.9.1-milestone]
|
||||
**Date**: [3rd March 2019][3.9.2-milestone]
|
||||
|
||||
* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407]
|
||||
* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416]
|
||||
|
@ -294,7 +321,7 @@ You can determine your currently installed version using `pip show`:
|
|||
Note: `AutoSchema.__init__` now ensures `manual_fields` is a list.
|
||||
Previously may have been stored internally as `None`.
|
||||
|
||||
* Remove ulrparse compatability shim; use six instead [#5579][gh5579]
|
||||
* Remove ulrparse compatibility shim; use six instead [#5579][gh5579]
|
||||
* Drop compat wrapper for `TimeDelta.total_seconds()` [#5577][gh5577]
|
||||
* Clean up all whitespace throughout project [#5578][gh5578]
|
||||
* Compat cleanup [#5581][gh5581]
|
||||
|
@ -1165,7 +1192,8 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
|
||||
[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1
|
||||
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1
|
||||
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1
|
||||
[3.9.2-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1
|
||||
[3.10.0-milestone]: https://github.com/encode/django-rest-framework/milestone/69?closed=1
|
||||
|
||||
<!-- 3.0.1 -->
|
||||
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
|
||||
|
@ -2106,3 +2134,10 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh6340]: https://github.com/encode/django-rest-framework/issues/6340
|
||||
[gh6416]: https://github.com/encode/django-rest-framework/issues/6416
|
||||
[gh6407]: https://github.com/encode/django-rest-framework/issues/6407
|
||||
|
||||
<!-- 3.9.3 -->
|
||||
[gh6613]: https://github.com/encode/django-rest-framework/issues/6613
|
||||
|
||||
<!-- 3.10.0 -->
|
||||
[gh6680]: https://github.com/encode/django-rest-framework/issues/6680
|
||||
[gh6317]: https://github.com/encode/django-rest-framework/issues/6317
|
||||
|
|
|
@ -20,7 +20,7 @@ If you have an idea for a new feature please consider how it may be packaged as
|
|||
|
||||
You can use [this cookiecutter template][cookiecutter] for creating reusable Django REST Framework packages quickly. Cookiecutter creates projects from project templates. While optional, this cookiecutter template includes best practices from Django REST framework and other packages, as well as a Travis CI configuration, Tox configuration, and a sane setup.py for easy PyPI registration/distribution.
|
||||
|
||||
Note: Let us know if you have an alternate cookiecuter package so we can also link to it.
|
||||
Note: Let us know if you have an alternate cookiecutter package so we can also link to it.
|
||||
|
||||
#### Running the initial cookiecutter command
|
||||
|
||||
|
@ -197,6 +197,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [djangorestframework-composed-permissions][djangorestframework-composed-permissions] - Provides a simple way to define complex permissions.
|
||||
* [rest_condition][rest-condition] - Another extension for building complex permissions in a simple and convenient way.
|
||||
* [dry-rest-permissions][dry-rest-permissions] - Provides a simple way to define permissions for individual api actions.
|
||||
* [drf-access-policy][drf-access-policy] - Declarative and flexible permissions inspired by AWS' IAM policies.
|
||||
|
||||
### Serializers
|
||||
|
||||
|
@ -208,6 +209,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [django-rest-framework-serializer-extensions][drf-serializer-extensions] -
|
||||
Enables black/whitelisting fields, and conditionally expanding child serializers on a per-view/request basis.
|
||||
* [djangorestframework-queryfields][djangorestframework-queryfields] - Serializer mixin allowing clients to control which fields will be sent in the API response.
|
||||
* [drf-flex-fields][drf-flex-fields] - Serializer providing dynamic field expansion and sparse field sets via URL parameters.
|
||||
|
||||
### Serializer fields
|
||||
|
||||
|
@ -244,6 +246,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters.
|
||||
* [django-url-filter][django-url-filter] - Allows a safe way to filter data via human-friendly URLs. It is a generic library which is not tied to DRF but it provides easy integration with DRF.
|
||||
* [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values.
|
||||
* [django-rest-framework-guardian][django-rest-framework-guardian] - Provides integration with django-guardian, including the `DjangoObjectPermissionsFilter` previously found in DRF.
|
||||
|
||||
### Misc
|
||||
|
||||
|
@ -264,6 +267,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
|
||||
* [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net).
|
||||
* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified).
|
||||
* [django-rest-witchcraft][django-rest-witchcraft] - Provides DRF integration with SQLAlchemy with SQLAlchemy model serializers/viewsets and a bunch of other goodies
|
||||
* [djangorestframework-mvt][djangorestframework-mvt] - An extension for creating views that serve Postgres data as Map Box Vector Tiles.
|
||||
|
||||
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
|
||||
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
|
||||
|
@ -338,3 +343,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
|
||||
[djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables
|
||||
[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition
|
||||
[django-rest-witchcraft]: https://github.com/shosca/django-rest-witchcraft
|
||||
[drf-access-policy]: https://github.com/rsinger86/drf-access-policy
|
||||
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
|
||||
[djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt
|
||||
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||
|
|
Before Width: | Height: | Size: 567 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.9 KiB |
BIN
docs/img/premium/esg-readme.png
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
@ -68,7 +68,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
<ul class="premium-promo promo">
|
||||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||
<li><a href="https://releasehistory.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/release-history.png)">Release History</a></li>
|
||||
<li><a href="https://software.esg-usa.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/esg-new-logo.png)">ESG</a></li>
|
||||
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
||||
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
||||
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li>
|
||||
|
@ -76,7 +76,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
</ul>
|
||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||
|
||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
|
||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
|
||||
|
||||
---
|
||||
|
||||
|
@ -84,7 +84,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
|
||||
REST framework requires the following:
|
||||
|
||||
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||
* Python (3.5, 3.6, 3.7)
|
||||
* Django (1.11, 2.0, 2.1, 2.2)
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
|
@ -93,9 +93,9 @@ each Python and Django series.
|
|||
The following packages are optional:
|
||||
|
||||
* [coreapi][coreapi] (1.32.0+) - Schema generation support.
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
||||
* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
|
||||
* [Pygments][pygments] (2.4.0+) - Add sytax highlighting to Markdown processing.
|
||||
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
||||
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
||||
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
||||
|
||||
## Installation
|
||||
|
@ -238,8 +238,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[eventbrite]: https://www.eventbrite.co.uk/about/
|
||||
[coreapi]: https://pypi.org/project/coreapi/
|
||||
[markdown]: https://pypi.org/project/Markdown/
|
||||
[pygments]: https://pypi.org/project/Pygments/
|
||||
[django-filter]: https://pypi.org/project/django-filter/
|
||||
[django-crispy-forms]: https://github.com/maraujop/django-crispy-forms
|
||||
[django-guardian]: https://github.com/django-guardian/django-guardian
|
||||
[index]: .
|
||||
[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
|
||||
|
|
|
@ -17,7 +17,7 @@ The built-in API documentation includes:
|
|||
### Installation
|
||||
|
||||
The `coreapi` library is required as a dependency for the API docs. Make sure
|
||||
to install the latest version. The `pygments` and `markdown` libraries
|
||||
to install the latest version. The `Pygments` and `Markdown` libraries
|
||||
are optional but recommended.
|
||||
|
||||
To install the API documentation, you'll need to include it in your project's URLconf:
|
||||
|
@ -195,18 +195,6 @@ This also translates into a very useful interactive documentation viewer in the
|
|||
|
||||
---
|
||||
|
||||
#### DRF Docs
|
||||
|
||||
[DRF Docs][drfdocs-repo] allows you to document Web APIs made with Django REST Framework and it is authored by Emmanouil Konstantinidis. It's made to work out of the box and its setup should not take more than a couple of minutes. Complete documentation can be found on the [website][drfdocs-website] while there is also a [demo][drfdocs-demo] available for people to see what it looks like. **Live API Endpoints** allow you to utilize the endpoints from within the documentation in a neat way.
|
||||
|
||||
Features include customizing the template with your branding, settings for hiding the docs depending on the environment and more.
|
||||
|
||||
Both this package and Django REST Swagger are fully documented, well supported, and come highly recommended.
|
||||
|
||||
![Screenshot - DRF docs][image-drf-docs]
|
||||
|
||||
---
|
||||
|
||||
#### Django REST Swagger
|
||||
|
||||
Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints.
|
||||
|
@ -215,7 +203,7 @@ Django REST Swagger supports REST framework versions 2.3 and above.
|
|||
|
||||
Mark is also the author of the [REST Framework Docs][rest-framework-docs] package which offers clean, simple autogenerated documentation for your API but is deprecated and has moved to Django REST Swagger.
|
||||
|
||||
Both this package and DRF docs are fully documented, well supported, and come highly recommended.
|
||||
This package is fully documented, well supported, and comes highly recommended.
|
||||
|
||||
![Screenshot - Django REST Swagger][image-django-rest-swagger]
|
||||
|
||||
|
@ -277,7 +265,7 @@ When working with viewsets, an appropriate suffix is appended to each generated
|
|||
|
||||
The description in the browsable API is generated from the docstring of the view or viewset.
|
||||
|
||||
If the python `markdown` library is installed, then [markdown syntax][markdown] may be used in the docstring, and will be converted to HTML in the browsable API. For example:
|
||||
If the python `Markdown` library is installed, then [markdown syntax][markdown] may be used in the docstring, and will be converted to HTML in the browsable API. For example:
|
||||
|
||||
class AccountListView(views.APIView):
|
||||
"""
|
||||
|
@ -322,18 +310,14 @@ To implement a hypermedia API you'll need to decide on an appropriate media type
|
|||
[cite]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||
[image-drf-yasg]: ../img/drf-yasg.png
|
||||
[drfdocs-repo]: https://github.com/ekonstantinidis/django-rest-framework-docs
|
||||
[drfdocs-website]: https://www.drfdocs.com/
|
||||
[drfdocs-demo]: http://demo.drfdocs.com/
|
||||
[drfautodocs-repo]: https://github.com/iMakedonsky/drf-autodocs
|
||||
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
|
||||
[swagger]: https://swagger.io/
|
||||
[open-api]: https://openapis.org/
|
||||
[rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||
[apiary]: https://apiary.io/
|
||||
[markdown]: https://daringfireball.net/projects/markdown/
|
||||
[markdown]: https://daringfireball.net/projects/markdown/syntax
|
||||
[hypermedia-docs]: rest-hypermedia-hateoas.md
|
||||
[image-drf-docs]: ../img/drfdocs.png
|
||||
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
||||
[image-apiary]: ../img/apiary.png
|
||||
[image-self-describing-api]: ../img/self-describing.png
|
||||
|
|
|
@ -14,18 +14,18 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
|||
|
||||
## Setting up a new environment
|
||||
|
||||
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
|
||||
Before we do anything else we'll create a new virtual environment, using [venv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
|
||||
|
||||
virtualenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
Now that we're inside a virtualenv environment, we can install our package requirements.
|
||||
Now that we're inside a virtual environment, we can install our package requirements.
|
||||
|
||||
pip install django
|
||||
pip install djangorestframework
|
||||
pip install pygments # We'll be using this for the code highlighting
|
||||
|
||||
**Note:** To exit the virtualenv environment at any time, just type `deactivate`. For more information see the [virtualenv documentation][virtualenv].
|
||||
**Note:** To exit the virtual environment at any time, just type `deactivate`. For more information see the [venv documentation][venv].
|
||||
|
||||
## Getting started
|
||||
|
||||
|
@ -372,7 +372,7 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
|||
[quickstart]: quickstart.md
|
||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||
[sandbox]: https://restframework.herokuapp.com/
|
||||
[virtualenv]: http://www.virtualenv.org/en/latest/index.html
|
||||
[venv]: https://docs.python.org/3/library/venv.html
|
||||
[tut-2]: 2-requests-and-responses.md
|
||||
[httpie]: https://github.com/jakubroztocil/httpie#installation
|
||||
[curl]: https://curl.haxx.se/
|
||||
|
|
|
@ -10,11 +10,11 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
|||
mkdir tutorial
|
||||
cd tutorial
|
||||
|
||||
# Create a virtualenv to isolate our package dependencies locally
|
||||
virtualenv env
|
||||
# Create a virtual environment to isolate our package dependencies locally
|
||||
python3 -m venv env
|
||||
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||
|
||||
# Install Django and Django REST framework into the virtualenv
|
||||
# Install Django and Django REST framework into the virtual environment
|
||||
pip install django
|
||||
pip install djangorestframework
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
|
||||
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
|
||||
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
|
||||
<script src="https://fund.django-rest-framework.org/sidebar_include.js"></script>
|
||||
<script async src="https://fund.django-rest-framework.org/sidebar_include.js"></script>
|
||||
<script>var base_url = '{{ base_url }}';</script>
|
||||
<script src="{{ base_url }}/mkdocs/js/require.js"></script>
|
||||
<script src="{{ base_url }}/js/theme.js"></script>
|
||||
|
|
|
@ -4,13 +4,15 @@ site_description: Django REST framework - Web APIs for Django
|
|||
|
||||
repo_url: https://github.com/encode/django-rest-framework
|
||||
|
||||
theme_dir: docs_theme
|
||||
theme:
|
||||
name: mkdocs
|
||||
custom_dir: docs_theme
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
anchorlink: True
|
||||
|
||||
pages:
|
||||
nav:
|
||||
- Home: 'index.md'
|
||||
- Tutorial:
|
||||
- 'Quickstart': 'tutorial/quickstart.md'
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
# MkDocs to build our documentation.
|
||||
mkdocs==0.16.3
|
||||
mkdocs==1.0.4
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Optional packages which may be used with REST framework.
|
||||
psycopg2-binary==2.7.5
|
||||
markdown==2.6.11
|
||||
psycopg2-binary>=2.8.2, <2.9
|
||||
markdown==3.1.1
|
||||
pygments==2.4.2
|
||||
django-guardian==1.5.0
|
||||
django-filter==1.1.0
|
||||
django-filter>=2.1.0, <2.2
|
||||
coreapi==2.3.1
|
||||
coreschema==0.0.4
|
||||
pyyaml
|
||||
pyyaml>=5.1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Pytest for running the tests.
|
||||
pytest==4.3.0
|
||||
pytest-django==3.4.8
|
||||
pytest-cov==2.6.1
|
||||
pytest>=5.0,<5.1
|
||||
pytest-django>=3.5.1,<3.6
|
||||
pytest-cov>=2.7.1
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.9.2'
|
||||
__version__ = '3.9.3'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
|
||||
|
@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601'
|
|||
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
|
||||
|
||||
|
||||
class RemovedInDRF310Warning(DeprecationWarning):
|
||||
class RemovedInDRF311Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInDRF311Warning(PendingDeprecationWarning):
|
||||
class RemovedInDRF312Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
"""
|
||||
Provides various authentication policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from django.utils.six import text_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING, exceptions
|
||||
|
||||
|
@ -21,7 +18,7 @@ def get_authorization_header(request):
|
|||
Hide some test client ickyness where the header can be unicode.
|
||||
"""
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', b'')
|
||||
if isinstance(auth, text_type):
|
||||
if isinstance(auth, str):
|
||||
# Work around django test client oddness
|
||||
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||
return auth
|
||||
|
@ -33,7 +30,7 @@ class CSRFCheck(CsrfViewMiddleware):
|
|||
return reason
|
||||
|
||||
|
||||
class BaseAuthentication(object):
|
||||
class BaseAuthentication:
|
||||
"""
|
||||
All authentication classes should extend BaseAuthentication.
|
||||
"""
|
||||
|
|
|
@ -7,6 +7,7 @@ class TokenAdmin(admin.ModelAdmin):
|
|||
list_display = ('key', 'user', 'created')
|
||||
fields = ('user',)
|
||||
ordering = ('-created',)
|
||||
autocomplete_fields = ('user',)
|
||||
|
||||
|
||||
admin.site.register(Token, TokenAdmin)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AuthTokenConfig(AppConfig):
|
||||
|
|
|
@ -38,8 +38,8 @@ class Command(BaseCommand):
|
|||
token = self.create_user_token(username, reset_token)
|
||||
except UserModel.DoesNotExist:
|
||||
raise CommandError(
|
||||
'Cannot create the Token: user {0} does not exist'.format(
|
||||
'Cannot create the Token: user {} does not exist'.format(
|
||||
username)
|
||||
)
|
||||
self.stdout.write(
|
||||
'Generated token {0} for user {1}'.format(token.key, username))
|
||||
'Generated token {} for user {}'.format(token.key, username))
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -3,11 +3,9 @@ import os
|
|||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Token(models.Model):
|
||||
"""
|
||||
The default authorization token model.
|
||||
|
@ -32,7 +30,7 @@ class Token(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
if not self.key:
|
||||
self.key = self.generate_key()
|
||||
return super(Token, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def generate_key(self):
|
||||
return binascii.hexlify(os.urandom(20)).decode()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
|
|
@ -2,23 +2,11 @@
|
|||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of Django/Python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.utils import six
|
||||
from django.views.generic import View
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
from collections.abc import Mapping, MutableMapping # noqa
|
||||
except ImportError:
|
||||
# Python 2.7
|
||||
from collections import Mapping, MutableMapping # noqa
|
||||
|
||||
try:
|
||||
from django.urls import ( # noqa
|
||||
URLPattern,
|
||||
|
@ -36,11 +24,6 @@ try:
|
|||
except ImportError:
|
||||
ProhibitNullCharactersValidator = None
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
mock = None
|
||||
|
||||
|
||||
def get_original_route(urlpattern):
|
||||
"""
|
||||
|
@ -89,23 +72,6 @@ def make_url_resolver(regex, urlpatterns):
|
|||
return URLResolver(regex, urlpatterns)
|
||||
|
||||
|
||||
def unicode_repr(instance):
|
||||
# Get the repr of an instance, but ensure it is a unicode string
|
||||
# on both python 3 (already the case) and 2 (not the case).
|
||||
if six.PY2:
|
||||
return repr(instance).decode('utf-8')
|
||||
return repr(instance)
|
||||
|
||||
|
||||
def unicode_to_repr(value):
|
||||
# Coerce a unicode string to the correct repr return type, depending on
|
||||
# the Python version. We wrap all our `__repr__` implementations with
|
||||
# this and then use unicode throughout internally.
|
||||
if six.PY2:
|
||||
return value.encode('utf-8')
|
||||
return value
|
||||
|
||||
|
||||
def unicode_http_header(value):
|
||||
# Coerce HTTP header value to unicode.
|
||||
if isinstance(value, bytes):
|
||||
|
@ -150,13 +116,6 @@ except ImportError:
|
|||
yaml = None
|
||||
|
||||
|
||||
# django-crispy-forms is optional
|
||||
try:
|
||||
import crispy_forms
|
||||
except ImportError:
|
||||
crispy_forms = None
|
||||
|
||||
|
||||
# requests is optional
|
||||
try:
|
||||
import requests
|
||||
|
@ -164,35 +123,17 @@ except ImportError:
|
|||
requests = None
|
||||
|
||||
|
||||
def is_guardian_installed():
|
||||
"""
|
||||
django-guardian is optional and only imported if in INSTALLED_APPS.
|
||||
"""
|
||||
if six.PY2:
|
||||
# Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7.
|
||||
# Remove when dropping PY2.
|
||||
return False
|
||||
return 'guardian' in settings.INSTALLED_APPS
|
||||
|
||||
|
||||
# PATCH method is not implemented by Django
|
||||
if 'patch' not in View.http_method_names:
|
||||
View.http_method_names = View.http_method_names + ['patch']
|
||||
|
||||
|
||||
# Markdown is optional
|
||||
# Markdown is optional (version 3.0+ required)
|
||||
try:
|
||||
import markdown
|
||||
|
||||
if markdown.version <= '2.2':
|
||||
HEADERID_EXT_PATH = 'headerid'
|
||||
LEVEL_PARAM = 'level'
|
||||
elif markdown.version < '2.6':
|
||||
HEADERID_EXT_PATH = 'markdown.extensions.headerid'
|
||||
LEVEL_PARAM = 'level'
|
||||
else:
|
||||
HEADERID_EXT_PATH = 'markdown.extensions.toc'
|
||||
LEVEL_PARAM = 'baselevel'
|
||||
HEADERID_EXT_PATH = 'markdown.extensions.toc'
|
||||
LEVEL_PARAM = 'baselevel'
|
||||
|
||||
def apply_markdown(text):
|
||||
"""
|
||||
|
@ -265,7 +206,7 @@ if markdown is not None and pygments is not None:
|
|||
return ret.split("\n")
|
||||
|
||||
def md_filter_add_syntax_highlight(md):
|
||||
md.preprocessors.add('highlight', CodeBlockPreprocessor(), "_begin")
|
||||
md.preprocessors.register(CodeBlockPreprocessor(), 'highlight', 40)
|
||||
return True
|
||||
else:
|
||||
def md_filter_add_syntax_highlight(md):
|
||||
|
@ -284,43 +225,9 @@ except ImportError:
|
|||
|
||||
# `separators` argument to `json.dumps()` differs between 2.x and 3.x
|
||||
# See: https://bugs.python.org/issue22767
|
||||
if six.PY3:
|
||||
SHORT_SEPARATORS = (',', ':')
|
||||
LONG_SEPARATORS = (', ', ': ')
|
||||
INDENT_SEPARATORS = (',', ': ')
|
||||
else:
|
||||
SHORT_SEPARATORS = (b',', b':')
|
||||
LONG_SEPARATORS = (b', ', b': ')
|
||||
INDENT_SEPARATORS = (b',', b': ')
|
||||
|
||||
|
||||
class CustomValidatorMessage(object):
|
||||
"""
|
||||
We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`.
|
||||
https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297
|
||||
|
||||
Ref: https://github.com/encode/django-rest-framework/pull/5452
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.message = kwargs.pop('message', self.message)
|
||||
super(CustomValidatorMessage, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MinValueValidator(CustomValidatorMessage, validators.MinValueValidator):
|
||||
pass
|
||||
|
||||
|
||||
class MaxValueValidator(CustomValidatorMessage, validators.MaxValueValidator):
|
||||
pass
|
||||
|
||||
|
||||
class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
|
||||
pass
|
||||
|
||||
|
||||
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
|
||||
pass
|
||||
SHORT_SEPARATORS = (',', ':')
|
||||
LONG_SEPARATORS = (', ', ': ')
|
||||
INDENT_SEPARATORS = (',', ': ')
|
||||
|
||||
|
||||
# Version Constants.
|
||||
|
|
|
@ -3,18 +3,13 @@ The most important decorator in this module is `@api_view`, which is used
|
|||
for writing function-based views with REST framework.
|
||||
|
||||
There are also various decorators for setting the API policies on function
|
||||
based views, as well as the `@detail_route` and `@list_route` decorators, which are
|
||||
used to annotate methods on viewsets that should be included by routers.
|
||||
based views, as well as the `@action` decorator, which is used to annotate
|
||||
methods on viewsets that should be included by routers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import types
|
||||
import warnings
|
||||
|
||||
from django.forms.utils import pretty_name
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import RemovedInDRF310Warning
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
|
@ -28,7 +23,7 @@ def api_view(http_method_names=None):
|
|||
def decorator(func):
|
||||
|
||||
WrappedAPIView = type(
|
||||
six.PY3 and 'WrappedAPIView' or b'WrappedAPIView',
|
||||
'WrappedAPIView',
|
||||
(APIView,),
|
||||
{'__doc__': func.__doc__}
|
||||
)
|
||||
|
@ -217,39 +212,3 @@ class MethodMapper(dict):
|
|||
|
||||
def trace(self, func):
|
||||
return self._map('trace', func)
|
||||
|
||||
|
||||
def detail_route(methods=None, **kwargs):
|
||||
"""
|
||||
Used to mark a method on a ViewSet that should be routed for detail requests.
|
||||
"""
|
||||
warnings.warn(
|
||||
"`detail_route` is deprecated and will be removed in 3.10 in favor of "
|
||||
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
|
||||
def decorator(func):
|
||||
func = action(methods, detail=True, **kwargs)(func)
|
||||
if 'url_name' not in kwargs:
|
||||
func.url_name = func.url_path.replace('_', '-')
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
def list_route(methods=None, **kwargs):
|
||||
"""
|
||||
Used to mark a method on a ViewSet that should be routed for list requests.
|
||||
"""
|
||||
warnings.warn(
|
||||
"`list_route` is deprecated and will be removed in 3.10 in favor of "
|
||||
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
|
||||
def decorator(func):
|
||||
func = action(methods, detail=False, **kwargs)(func)
|
||||
if 'url_name' not in kwargs:
|
||||
func.url_name = func.url_path.replace('_', '-')
|
||||
return func
|
||||
return decorator
|
||||
|
|
|
@ -4,18 +4,14 @@ Handled exceptions raised by REST framework.
|
|||
In addition Django's built in 403 and 404 exceptions are handled.
|
||||
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import math
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.compat import unicode_to_repr
|
||||
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList
|
||||
|
||||
|
||||
|
@ -64,19 +60,19 @@ def _get_full_details(detail):
|
|||
}
|
||||
|
||||
|
||||
class ErrorDetail(six.text_type):
|
||||
class ErrorDetail(str):
|
||||
"""
|
||||
A string-like object that can additionally have a code.
|
||||
"""
|
||||
code = None
|
||||
|
||||
def __new__(cls, string, code=None):
|
||||
self = super(ErrorDetail, cls).__new__(cls, string)
|
||||
self = super().__new__(cls, string)
|
||||
self.code = code
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
r = super(ErrorDetail, self).__eq__(other)
|
||||
r = super().__eq__(other)
|
||||
try:
|
||||
return r and self.code == other.code
|
||||
except AttributeError:
|
||||
|
@ -86,10 +82,10 @@ class ErrorDetail(six.text_type):
|
|||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('ErrorDetail(string=%r, code=%r)' % (
|
||||
six.text_type(self),
|
||||
return 'ErrorDetail(string=%r, code=%r)' % (
|
||||
str(self),
|
||||
self.code,
|
||||
))
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(str(self))
|
||||
|
@ -113,7 +109,7 @@ class APIException(Exception):
|
|||
self.detail = _get_error_details(detail, code)
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.detail)
|
||||
return str(self.detail)
|
||||
|
||||
def get_codes(self):
|
||||
"""
|
||||
|
@ -196,7 +192,7 @@ class MethodNotAllowed(APIException):
|
|||
def __init__(self, method, detail=None, code=None):
|
||||
if detail is None:
|
||||
detail = force_text(self.default_detail).format(method=method)
|
||||
super(MethodNotAllowed, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class NotAcceptable(APIException):
|
||||
|
@ -206,7 +202,7 @@ class NotAcceptable(APIException):
|
|||
|
||||
def __init__(self, detail=None, code=None, available_renderers=None):
|
||||
self.available_renderers = available_renderers
|
||||
super(NotAcceptable, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class UnsupportedMediaType(APIException):
|
||||
|
@ -217,7 +213,7 @@ class UnsupportedMediaType(APIException):
|
|||
def __init__(self, media_type, detail=None, code=None):
|
||||
if detail is None:
|
||||
detail = force_text(self.default_detail).format(media_type=media_type)
|
||||
super(UnsupportedMediaType, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class Throttled(APIException):
|
||||
|
@ -234,11 +230,11 @@ class Throttled(APIException):
|
|||
wait = math.ceil(wait)
|
||||
detail = ' '.join((
|
||||
detail,
|
||||
force_text(ungettext(self.extra_detail_singular.format(wait=wait),
|
||||
self.extra_detail_plural.format(wait=wait),
|
||||
wait))))
|
||||
force_text(ngettext(self.extra_detail_singular.format(wait=wait),
|
||||
self.extra_detail_plural.format(wait=wait),
|
||||
wait))))
|
||||
self.wait = wait
|
||||
super(Throttled, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
def server_error(request, *args, **kwargs):
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -8,37 +6,35 @@ import inspect
|
|||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.core.validators import (
|
||||
EmailValidator, RegexValidator, URLValidator, ip_address_validators
|
||||
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, RegexValidator, URLValidator, ip_address_validators
|
||||
)
|
||||
from django.forms import FilePathField as DjangoFilePathField
|
||||
from django.forms import ImageField as DjangoImageField
|
||||
from django.utils import six, timezone
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import (
|
||||
parse_date, parse_datetime, parse_duration, parse_time
|
||||
)
|
||||
from django.utils.duration import duration_string
|
||||
from django.utils.encoding import is_protected_type, smart_text
|
||||
from django.utils.formats import localize_input, sanitize_separators
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.ipv6 import clean_ipv6_address
|
||||
from django.utils.timezone import utc
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pytz.exceptions import InvalidTimeError
|
||||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import (
|
||||
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
|
||||
unicode_to_repr
|
||||
)
|
||||
from rest_framework.compat import ProhibitNullCharactersValidator
|
||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html, humanize_datetime, json, representation
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
|
||||
|
||||
class empty:
|
||||
|
@ -51,39 +47,35 @@ class empty:
|
|||
pass
|
||||
|
||||
|
||||
if six.PY3:
|
||||
def is_simple_callable(obj):
|
||||
"""
|
||||
True if the object is a callable that takes no arguments.
|
||||
"""
|
||||
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
|
||||
return False
|
||||
class BuiltinSignatureError(Exception):
|
||||
"""
|
||||
Built-in function signatures are not inspectable. This exception is raised
|
||||
so the serializer can raise a helpful error message.
|
||||
"""
|
||||
pass
|
||||
|
||||
sig = inspect.signature(obj)
|
||||
params = sig.parameters.values()
|
||||
return all(
|
||||
param.kind == param.VAR_POSITIONAL or
|
||||
param.kind == param.VAR_KEYWORD or
|
||||
param.default != param.empty
|
||||
for param in params
|
||||
)
|
||||
|
||||
else:
|
||||
def is_simple_callable(obj):
|
||||
function = inspect.isfunction(obj)
|
||||
method = inspect.ismethod(obj)
|
||||
def is_simple_callable(obj):
|
||||
"""
|
||||
True if the object is a callable that takes no arguments.
|
||||
"""
|
||||
# Bail early since we cannot inspect built-in function signatures.
|
||||
if inspect.isbuiltin(obj):
|
||||
raise BuiltinSignatureError(
|
||||
'Built-in function signatures are not inspectable. '
|
||||
'Wrap the function call in a simple, pure Python function.')
|
||||
|
||||
if not (function or method):
|
||||
return False
|
||||
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
|
||||
return False
|
||||
|
||||
if method:
|
||||
is_unbound = obj.im_self is None
|
||||
|
||||
args, _, _, defaults = inspect.getargspec(obj)
|
||||
|
||||
len_args = len(args) if function or is_unbound else len(args) - 1
|
||||
len_defaults = len(defaults) if defaults else 0
|
||||
return len_args <= len_defaults
|
||||
sig = inspect.signature(obj)
|
||||
params = sig.parameters.values()
|
||||
return all(
|
||||
param.kind == param.VAR_POSITIONAL or
|
||||
param.kind == param.VAR_KEYWORD or
|
||||
param.default != param.empty
|
||||
for param in params
|
||||
)
|
||||
|
||||
|
||||
def get_attribute(instance, attrs):
|
||||
|
@ -108,7 +100,7 @@ def get_attribute(instance, attrs):
|
|||
# If we raised an Attribute or KeyError here it'd get treated
|
||||
# as an omitted field in `Field.get_attribute()`. Instead we
|
||||
# raise a ValueError to ensure the exception is not masked.
|
||||
raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc))
|
||||
raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc))
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -185,18 +177,18 @@ def iter_options(grouped_choices, cutoff=None, cutoff_text=None):
|
|||
"""
|
||||
Helper function for options and option groups in templates.
|
||||
"""
|
||||
class StartOptionGroup(object):
|
||||
class StartOptionGroup:
|
||||
start_option_group = True
|
||||
end_option_group = False
|
||||
|
||||
def __init__(self, label):
|
||||
self.label = label
|
||||
|
||||
class EndOptionGroup(object):
|
||||
class EndOptionGroup:
|
||||
start_option_group = False
|
||||
end_option_group = True
|
||||
|
||||
class Option(object):
|
||||
class Option:
|
||||
start_option_group = False
|
||||
end_option_group = False
|
||||
|
||||
|
@ -239,19 +231,19 @@ def get_error_detail(exc_info):
|
|||
error_dict = exc_info.error_dict
|
||||
except AttributeError:
|
||||
return [
|
||||
ErrorDetail(error.message % (error.params or ()),
|
||||
ErrorDetail((error.message % error.params) if error.params else error.message,
|
||||
code=error.code if error.code else code)
|
||||
for error in exc_info.error_list]
|
||||
return {
|
||||
k: [
|
||||
ErrorDetail(error.message % (error.params or ()),
|
||||
ErrorDetail((error.message % error.params) if error.params else error.message,
|
||||
code=error.code if error.code else code)
|
||||
for error in errors
|
||||
] for k, errors in error_dict.items()
|
||||
}
|
||||
|
||||
|
||||
class CreateOnlyDefault(object):
|
||||
class CreateOnlyDefault:
|
||||
"""
|
||||
This class may be used to provide default values that are only used
|
||||
for create operations, but that do not return any value for update
|
||||
|
@ -273,12 +265,10 @@ class CreateOnlyDefault(object):
|
|||
return self.default
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr(
|
||||
'%s(%s)' % (self.__class__.__name__, unicode_repr(self.default))
|
||||
)
|
||||
return '%s(%s)' % (self.__class__.__name__, repr(self.default))
|
||||
|
||||
|
||||
class CurrentUserDefault(object):
|
||||
class CurrentUserDefault:
|
||||
def set_context(self, serializer_field):
|
||||
self.user = serializer_field.context['request'].user
|
||||
|
||||
|
@ -286,7 +276,7 @@ class CurrentUserDefault(object):
|
|||
return self.user
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('%s()' % self.__class__.__name__)
|
||||
return '%s()' % self.__class__.__name__
|
||||
|
||||
|
||||
class SkipField(Exception):
|
||||
|
@ -305,7 +295,7 @@ MISSING_ERROR_MESSAGE = (
|
|||
)
|
||||
|
||||
|
||||
class Field(object):
|
||||
class Field:
|
||||
_creation_counter = 0
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -451,6 +441,18 @@ class Field(object):
|
|||
"""
|
||||
try:
|
||||
return get_attribute(instance, self.source_attrs)
|
||||
except BuiltinSignatureError as exc:
|
||||
msg = (
|
||||
'Field source for `{serializer}.{field}` maps to a built-in '
|
||||
'function type and is invalid. Define a property or method on '
|
||||
'the `{instance}` instance that wraps the call to the built-in '
|
||||
'function.'.format(
|
||||
serializer=self.parent.__class__.__name__,
|
||||
field=self.field_name,
|
||||
instance=instance.__class__.__name__,
|
||||
)
|
||||
)
|
||||
raise type(exc)(msg)
|
||||
except (KeyError, AttributeError) as exc:
|
||||
if self.default is not empty:
|
||||
return self.get_default()
|
||||
|
@ -515,6 +517,11 @@ class Field(object):
|
|||
if data is None:
|
||||
if not self.allow_null:
|
||||
self.fail('null')
|
||||
# Nullable `source='*'` fields should not be skipped when its named
|
||||
# field is given a null value. This is because `source='*'` means
|
||||
# the field is passed the entire object, which is not null.
|
||||
elif self.source == '*':
|
||||
return (False, None)
|
||||
return (True, None)
|
||||
|
||||
return (False, data)
|
||||
|
@ -618,7 +625,7 @@ class Field(object):
|
|||
When a field is instantiated, we store the arguments that were used,
|
||||
so that we can present a helpful representation of the object.
|
||||
"""
|
||||
instance = super(Field, cls).__new__(cls)
|
||||
instance = super().__new__(cls)
|
||||
instance._args = args
|
||||
instance._kwargs = kwargs
|
||||
return instance
|
||||
|
@ -636,7 +643,7 @@ class Field(object):
|
|||
for item in self._args
|
||||
]
|
||||
kwargs = {
|
||||
key: (copy.deepcopy(value) if (key not in ('validators', 'regex')) else value)
|
||||
key: (copy.deepcopy(value, memo) if (key not in ('validators', 'regex')) else value)
|
||||
for key, value in self._kwargs.items()
|
||||
}
|
||||
return self.__class__(*args, **kwargs)
|
||||
|
@ -647,7 +654,7 @@ class Field(object):
|
|||
This allows us to create descriptive representations for serializer
|
||||
instances that show all the declared fields on the serializer.
|
||||
"""
|
||||
return unicode_to_repr(representation.field_repr(self))
|
||||
return representation.field_repr(self)
|
||||
|
||||
|
||||
# Boolean types...
|
||||
|
@ -724,7 +731,7 @@ class NullBooleanField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
|
||||
kwargs['allow_null'] = True
|
||||
super(NullBooleanField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -764,17 +771,13 @@ class CharField(Field):
|
|||
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.min_length = kwargs.pop('min_length', None)
|
||||
super(CharField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if self.max_length is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_length'].format,
|
||||
six.text_type)(max_length=self.max_length)
|
||||
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
|
||||
self.validators.append(
|
||||
MaxLengthValidator(self.max_length, message=message))
|
||||
if self.min_length is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_length'].format,
|
||||
six.text_type)(min_length=self.min_length)
|
||||
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
|
||||
self.validators.append(
|
||||
MinLengthValidator(self.min_length, message=message))
|
||||
|
||||
|
@ -786,23 +789,23 @@ class CharField(Field):
|
|||
# Test for the empty string here so that it does not get validated,
|
||||
# and so that subclasses do not need to handle it explicitly
|
||||
# inside the `to_internal_value()` method.
|
||||
if data == '' or (self.trim_whitespace and six.text_type(data).strip() == ''):
|
||||
if data == '' or (self.trim_whitespace and str(data).strip() == ''):
|
||||
if not self.allow_blank:
|
||||
self.fail('blank')
|
||||
return ''
|
||||
return super(CharField, self).run_validation(data)
|
||||
return super().run_validation(data)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# We're lenient with allowing basic numerics to be coerced into strings,
|
||||
# but other types should fail. Eg. unclear if booleans should represent as `true` or `True`,
|
||||
# and composites such as lists are likely user error.
|
||||
if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)):
|
||||
if isinstance(data, bool) or not isinstance(data, (str, int, float,)):
|
||||
self.fail('invalid')
|
||||
value = six.text_type(data)
|
||||
value = str(data)
|
||||
return value.strip() if self.trim_whitespace else value
|
||||
|
||||
def to_representation(self, value):
|
||||
return six.text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
class EmailField(CharField):
|
||||
|
@ -811,7 +814,7 @@ class EmailField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmailField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = EmailValidator(message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -822,7 +825,7 @@ class RegexField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, regex, **kwargs):
|
||||
super(RegexField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = RegexValidator(regex, message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -834,7 +837,7 @@ class SlugField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, allow_unicode=False, **kwargs):
|
||||
super(SlugField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.allow_unicode = allow_unicode
|
||||
if self.allow_unicode:
|
||||
validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode'])
|
||||
|
@ -849,7 +852,7 @@ class URLField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = URLValidator(message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -866,16 +869,16 @@ class UUIDField(Field):
|
|||
if self.uuid_format not in self.valid_formats:
|
||||
raise ValueError(
|
||||
'Invalid format for uuid representation. '
|
||||
'Must be one of "{0}"'.format('", "'.join(self.valid_formats))
|
||||
'Must be one of "{}"'.format('", "'.join(self.valid_formats))
|
||||
)
|
||||
super(UUIDField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, uuid.UUID):
|
||||
try:
|
||||
if isinstance(data, six.integer_types):
|
||||
if isinstance(data, int):
|
||||
return uuid.UUID(int=data)
|
||||
elif isinstance(data, six.string_types):
|
||||
elif isinstance(data, str):
|
||||
return uuid.UUID(hex=data)
|
||||
else:
|
||||
self.fail('invalid', value=data)
|
||||
|
@ -900,12 +903,12 @@ class IPAddressField(CharField):
|
|||
def __init__(self, protocol='both', **kwargs):
|
||||
self.protocol = protocol.lower()
|
||||
self.unpack_ipv4 = (self.protocol == 'both')
|
||||
super(IPAddressField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
|
||||
self.validators.extend(validators)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, six.string_types):
|
||||
if not isinstance(data, str):
|
||||
self.fail('invalid', value=data)
|
||||
|
||||
if ':' in data:
|
||||
|
@ -915,7 +918,7 @@ class IPAddressField(CharField):
|
|||
except DjangoValidationError:
|
||||
self.fail('invalid', value=data)
|
||||
|
||||
return super(IPAddressField, self).to_internal_value(data)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
# Number types...
|
||||
|
@ -933,22 +936,18 @@ class IntegerField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_value', None)
|
||||
self.min_value = kwargs.pop('min_value', None)
|
||||
super(IntegerField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if self.max_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
|
||||
if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH:
|
||||
self.fail('max_string_length')
|
||||
|
||||
try:
|
||||
|
@ -973,23 +972,19 @@ class FloatField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_value', None)
|
||||
self.min_value = kwargs.pop('min_value', None)
|
||||
super(FloatField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if self.max_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
||||
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
|
||||
if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH:
|
||||
self.fail('max_string_length')
|
||||
|
||||
try:
|
||||
|
@ -1031,18 +1026,14 @@ class DecimalField(Field):
|
|||
else:
|
||||
self.max_whole_digits = None
|
||||
|
||||
super(DecimalField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if self.max_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
|
@ -1121,7 +1112,7 @@ class DecimalField(Field):
|
|||
coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING)
|
||||
|
||||
if not isinstance(value, decimal.Decimal):
|
||||
value = decimal.Decimal(six.text_type(value).strip())
|
||||
value = decimal.Decimal(str(value).strip())
|
||||
|
||||
quantized = self.quantize(value)
|
||||
|
||||
|
@ -1130,7 +1121,7 @@ class DecimalField(Field):
|
|||
if self.localize:
|
||||
return localize_input(quantized)
|
||||
|
||||
return '{0:f}'.format(quantized)
|
||||
return '{:f}'.format(quantized)
|
||||
|
||||
def quantize(self, value):
|
||||
"""
|
||||
|
@ -1167,7 +1158,7 @@ class DateTimeField(Field):
|
|||
self.input_formats = input_formats
|
||||
if default_timezone is not None:
|
||||
self.timezone = default_timezone
|
||||
super(DateTimeField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def enforce_timezone(self, value):
|
||||
"""
|
||||
|
@ -1226,7 +1217,7 @@ class DateTimeField(Field):
|
|||
|
||||
output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT)
|
||||
|
||||
if output_format is None or isinstance(value, six.string_types):
|
||||
if output_format is None or isinstance(value, str):
|
||||
return value
|
||||
|
||||
value = self.enforce_timezone(value)
|
||||
|
@ -1251,7 +1242,7 @@ class DateField(Field):
|
|||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(DateField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
|
||||
|
@ -1288,7 +1279,7 @@ class DateField(Field):
|
|||
|
||||
output_format = getattr(self, 'format', api_settings.DATE_FORMAT)
|
||||
|
||||
if output_format is None or isinstance(value, six.string_types):
|
||||
if output_format is None or isinstance(value, str):
|
||||
return value
|
||||
|
||||
# Applying a `DateField` to a datetime value is almost always
|
||||
|
@ -1317,7 +1308,7 @@ class TimeField(Field):
|
|||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(TimeField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
|
||||
|
@ -1351,7 +1342,7 @@ class TimeField(Field):
|
|||
|
||||
output_format = getattr(self, 'format', api_settings.TIME_FORMAT)
|
||||
|
||||
if output_format is None or isinstance(value, six.string_types):
|
||||
if output_format is None or isinstance(value, str):
|
||||
return value
|
||||
|
||||
# Applying a `TimeField` to a datetime value is almost always
|
||||
|
@ -1378,24 +1369,20 @@ class DurationField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_value', None)
|
||||
self.min_value = kwargs.pop('min_value', None)
|
||||
super(DurationField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if self.max_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
def to_internal_value(self, value):
|
||||
if isinstance(value, datetime.timedelta):
|
||||
return value
|
||||
parsed = parse_duration(six.text_type(value))
|
||||
parsed = parse_duration(str(value))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
|
||||
|
@ -1420,21 +1407,21 @@ class ChoiceField(Field):
|
|||
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
|
||||
super(ChoiceField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data == '' and self.allow_blank:
|
||||
return ''
|
||||
|
||||
try:
|
||||
return self.choice_strings_to_values[six.text_type(data)]
|
||||
return self.choice_strings_to_values[str(data)]
|
||||
except KeyError:
|
||||
self.fail('invalid_choice', input=data)
|
||||
|
||||
def to_representation(self, value):
|
||||
if value in ('', None):
|
||||
return value
|
||||
return self.choice_strings_to_values.get(six.text_type(value), value)
|
||||
return self.choice_strings_to_values.get(str(value), value)
|
||||
|
||||
def iter_options(self):
|
||||
"""
|
||||
|
@ -1457,7 +1444,7 @@ class ChoiceField(Field):
|
|||
# Allows us to deal with eg. integer choices while supporting either
|
||||
# integer or string input, but still get the correct datatype out.
|
||||
self.choice_strings_to_values = {
|
||||
six.text_type(key): key for key in self.choices
|
||||
str(key): key for key in self.choices
|
||||
}
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
@ -1473,7 +1460,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
super(MultipleChoiceField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
if self.field_name not in dictionary:
|
||||
|
@ -1486,7 +1473,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1498,7 +1485,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
|
||||
def to_representation(self, value):
|
||||
return {
|
||||
self.choice_strings_to_values.get(six.text_type(item), item) for item in value
|
||||
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||
}
|
||||
|
||||
|
||||
|
@ -1516,7 +1503,7 @@ class FilePathField(ChoiceField):
|
|||
allow_folders=allow_folders, required=required
|
||||
)
|
||||
kwargs['choices'] = field.choices
|
||||
super(FilePathField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
# File types...
|
||||
|
@ -1535,7 +1522,7 @@ class FileField(Field):
|
|||
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||
if 'use_url' in kwargs:
|
||||
self.use_url = kwargs.pop('use_url')
|
||||
super(FileField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -1581,13 +1568,13 @@ class ImageField(FileField):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
|
||||
super(ImageField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Image validation is a bit grungy, so we'll just outright
|
||||
# defer to Django's implementation so we don't need to
|
||||
# consider it, or treat PIL as a test dependency.
|
||||
file_object = super(ImageField, self).to_internal_value(data)
|
||||
file_object = super().to_internal_value(data)
|
||||
django_field = self._DjangoImageField()
|
||||
django_field.error_messages = self.error_messages
|
||||
return django_field.clean(file_object)
|
||||
|
@ -1597,7 +1584,7 @@ class ImageField(FileField):
|
|||
|
||||
class _UnvalidatedField(Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_UnvalidatedField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.allow_blank = True
|
||||
self.allow_null = True
|
||||
|
||||
|
@ -1630,13 +1617,13 @@ class ListField(Field):
|
|||
"Remove `source=` from the field declaration."
|
||||
)
|
||||
|
||||
super(ListField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
if self.max_length is not None:
|
||||
message = self.error_messages['max_length'].format(max_length=self.max_length)
|
||||
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
|
||||
self.validators.append(MaxLengthValidator(self.max_length, message=message))
|
||||
if self.min_length is not None:
|
||||
message = self.error_messages['min_length'].format(min_length=self.min_length)
|
||||
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
|
||||
self.validators.append(MinLengthValidator(self.min_length, message=message))
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -1660,7 +1647,7 @@ class ListField(Field):
|
|||
"""
|
||||
if html.is_html_input(data):
|
||||
data = html.parse_html_list(data, default=[])
|
||||
if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, (str, Mapping)) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1691,11 +1678,13 @@ class DictField(Field):
|
|||
child = _UnvalidatedField()
|
||||
initial = {}
|
||||
default_error_messages = {
|
||||
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".')
|
||||
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'),
|
||||
'empty': _('This dictionary may not be empty.'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.child = kwargs.pop('child', copy.deepcopy(self.child))
|
||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
|
||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||
assert self.child.source is None, (
|
||||
|
@ -1703,7 +1692,7 @@ class DictField(Field):
|
|||
"Remove `source=` from the field declaration."
|
||||
)
|
||||
|
||||
super(DictField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -1721,11 +1710,14 @@ class DictField(Field):
|
|||
data = html.parse_html_dict(data)
|
||||
if not isinstance(data, dict):
|
||||
self.fail('not_a_dict', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
return self.run_child_validation(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
return {
|
||||
six.text_type(key): self.child.to_representation(val) if val is not None else None
|
||||
str(key): self.child.to_representation(val) if val is not None else None
|
||||
for key, val in value.items()
|
||||
}
|
||||
|
||||
|
@ -1734,7 +1726,7 @@ class DictField(Field):
|
|||
errors = OrderedDict()
|
||||
|
||||
for key, value in data.items():
|
||||
key = six.text_type(key)
|
||||
key = str(key)
|
||||
|
||||
try:
|
||||
result[key] = self.child.run_validation(value)
|
||||
|
@ -1750,7 +1742,7 @@ class HStoreField(DictField):
|
|||
child = CharField(allow_blank=True, allow_null=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HStoreField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
assert isinstance(self.child, CharField), (
|
||||
"The `child` argument must be an instance of `CharField`, "
|
||||
"as the hstore extension stores values as strings."
|
||||
|
@ -1764,15 +1756,16 @@ class JSONField(Field):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.binary = kwargs.pop('binary', False)
|
||||
super(JSONField, self).__init__(*args, **kwargs)
|
||||
self.encoder = kwargs.pop('encoder', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
||||
# When HTML form input is used, mark up the input
|
||||
# as being a JSON string, rather than a JSON primitive.
|
||||
class JSONString(six.text_type):
|
||||
class JSONString(str):
|
||||
def __new__(self, value):
|
||||
ret = six.text_type.__new__(self, value)
|
||||
ret = str.__new__(self, value)
|
||||
ret.is_json_string = True
|
||||
return ret
|
||||
return JSONString(dictionary[self.field_name])
|
||||
|
@ -1782,21 +1775,18 @@ class JSONField(Field):
|
|||
try:
|
||||
if self.binary or getattr(data, 'is_json_string', False):
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
data = data.decode()
|
||||
return json.loads(data)
|
||||
else:
|
||||
json.dumps(data)
|
||||
json.dumps(data, cls=self.encoder)
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.binary:
|
||||
value = json.dumps(value)
|
||||
# On python 2.x the return type for json.dumps() is underspecified.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(value, six.text_type):
|
||||
value = bytes(value.encode('utf-8'))
|
||||
value = json.dumps(value, cls=self.encoder)
|
||||
value = value.encode()
|
||||
return value
|
||||
|
||||
|
||||
|
@ -1817,7 +1807,7 @@ class ReadOnlyField(Field):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['read_only'] = True
|
||||
super(ReadOnlyField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
@ -1834,7 +1824,7 @@ class HiddenField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
assert 'default' in kwargs, 'default is a required argument.'
|
||||
kwargs['write_only'] = True
|
||||
super(HiddenField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We always use the default value for `HiddenField`.
|
||||
|
@ -1864,25 +1854,19 @@ class SerializerMethodField(Field):
|
|||
self.method_name = method_name
|
||||
kwargs['source'] = '*'
|
||||
kwargs['read_only'] = True
|
||||
super(SerializerMethodField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
# In order to enforce a consistent style, we error if a redundant
|
||||
# 'method_name' argument has been used. For example:
|
||||
# my_field = serializer.SerializerMethodField(method_name='get_my_field')
|
||||
default_method_name = 'get_{field_name}'.format(field_name=field_name)
|
||||
assert self.method_name != default_method_name, (
|
||||
"It is redundant to specify `%s` on SerializerMethodField '%s' in "
|
||||
"serializer '%s', because it is the same as the default method name. "
|
||||
"Remove the `method_name` argument." %
|
||||
(self.method_name, field_name, parent.__class__.__name__)
|
||||
)
|
||||
|
||||
# The method name should default to `get_{field_name}`.
|
||||
if self.method_name is None:
|
||||
self.method_name = default_method_name
|
||||
|
||||
super(SerializerMethodField, self).bind(field_name, parent)
|
||||
super().bind(field_name, parent)
|
||||
|
||||
def to_representation(self, value):
|
||||
method = getattr(self.parent, self.method_name)
|
||||
|
@ -1905,11 +1889,9 @@ class ModelField(Field):
|
|||
# The `max_length` option is supported by Django's base `Field` class,
|
||||
# so we'd better support it here.
|
||||
max_length = kwargs.pop('max_length', None)
|
||||
super(ModelField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if max_length is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_length'].format,
|
||||
six.text_type)(max_length=self.max_length)
|
||||
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
|
||||
self.validators.append(
|
||||
MaxLengthValidator(self.max_length, message=message))
|
||||
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
Provides generic filtering backends that can be used to filter the results
|
||||
returned by list views.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import operator
|
||||
import warnings
|
||||
from functools import reduce
|
||||
|
||||
from django import forms
|
||||
|
@ -14,14 +11,10 @@ from django.db import models
|
|||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.sql.constants import ORDER_PATTERN
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import RemovedInDRF310Warning
|
||||
from rest_framework.compat import (
|
||||
coreapi, coreschema, distinct, is_guardian_installed
|
||||
)
|
||||
from rest_framework.compat import coreapi, coreschema, distinct
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
|
@ -37,7 +30,7 @@ class SearchFilterForm(forms.Form):
|
|||
self.fields[search_field] = forms.CharField()
|
||||
|
||||
|
||||
class BaseFilterBackend(object):
|
||||
class BaseFilterBackend:
|
||||
"""
|
||||
A base class from which all filter backend classes should inherit.
|
||||
"""
|
||||
|
@ -53,6 +46,9 @@ class BaseFilterBackend(object):
|
|||
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
|
||||
return []
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the search.
|
||||
|
@ -127,7 +123,7 @@ class SearchFilter(BaseFilterBackend):
|
|||
return queryset
|
||||
|
||||
orm_lookups = [
|
||||
self.construct_search(six.text_type(search_field))
|
||||
self.construct_search(str(search_field))
|
||||
for search_field in search_fields
|
||||
]
|
||||
|
||||
|
@ -177,6 +173,19 @@ class SearchFilter(BaseFilterBackend):
|
|||
)
|
||||
]
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return [
|
||||
{
|
||||
'name': self.search_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.search_description),
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the ordering.
|
||||
|
@ -206,7 +215,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
|
||||
def get_default_ordering(self, view):
|
||||
ordering = getattr(view, 'ordering', None)
|
||||
if isinstance(ordering, six.string_types):
|
||||
if isinstance(ordering, str):
|
||||
return (ordering,)
|
||||
return ordering
|
||||
|
||||
|
@ -255,7 +264,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
]
|
||||
else:
|
||||
valid_fields = [
|
||||
(item, item) if isinstance(item, six.string_types) else item
|
||||
(item, item) if isinstance(item, str) else item
|
||||
for item in valid_fields
|
||||
]
|
||||
|
||||
|
@ -308,40 +317,15 @@ class OrderingFilter(BaseFilterBackend):
|
|||
)
|
||||
]
|
||||
|
||||
|
||||
class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||
"""
|
||||
A filter backend that limits results to those where the requesting user
|
||||
has read object level permissions.
|
||||
"""
|
||||
def __init__(self):
|
||||
warnings.warn(
|
||||
"`DjangoObjectPermissionsFilter` has been deprecated and moved to "
|
||||
"the 3rd-party django-rest-framework-guardian package.",
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
|
||||
|
||||
perm_format = '%(app_label)s.view_%(model_name)s'
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
# We want to defer this import until run-time, rather than import-time.
|
||||
# See https://github.com/encode/django-rest-framework/issues/4608
|
||||
# (Also see #1624 for why we need to make this import explicitly)
|
||||
from guardian import VERSION as guardian_version
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
extra = {}
|
||||
user = request.user
|
||||
model_cls = queryset.model
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
permission = self.perm_format % kwargs
|
||||
if tuple(guardian_version) >= (1, 3):
|
||||
# Maintain behavior compatibility with versions prior to 1.3
|
||||
extra = {'accept_global_perms': False}
|
||||
else:
|
||||
extra = {}
|
||||
return get_objects_for_user(user, permission, queryset, **extra)
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return [
|
||||
{
|
||||
'name': self.ordering_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.ordering_description),
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Generic views that provide commonly needed behaviour.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
|
|
|
@ -1,41 +1,63 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.renderers import (
|
||||
CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer
|
||||
)
|
||||
from rest_framework.schemas.generators import SchemaGenerator
|
||||
from rest_framework import renderers
|
||||
from rest_framework.schemas import coreapi
|
||||
from rest_framework.schemas.openapi import SchemaGenerator
|
||||
|
||||
OPENAPI_MODE = 'openapi'
|
||||
COREAPI_MODE = 'coreapi'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generates configured API schema for project."
|
||||
|
||||
def get_mode(self):
|
||||
return COREAPI_MODE if coreapi.is_enabled() else OPENAPI_MODE
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--title', dest="title", default=None, type=str)
|
||||
parser.add_argument('--title', dest="title", default='', type=str)
|
||||
parser.add_argument('--url', dest="url", default=None, type=str)
|
||||
parser.add_argument('--description', dest="description", default=None, type=str)
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
|
||||
else:
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)
|
||||
parser.add_argument('--urlconf', dest="urlconf", default=None, type=str)
|
||||
parser.add_argument('--generator_class', dest="generator_class", default=None, type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
assert coreapi is not None, 'coreapi must be installed.'
|
||||
|
||||
generator = SchemaGenerator(
|
||||
if options['generator_class']:
|
||||
generator_class = import_string(options['generator_class'])
|
||||
else:
|
||||
generator_class = self.get_generator_class()
|
||||
generator = generator_class(
|
||||
url=options['url'],
|
||||
title=options['title'],
|
||||
description=options['description']
|
||||
description=options['description'],
|
||||
urlconf=options['urlconf'],
|
||||
)
|
||||
|
||||
schema = generator.get_schema(request=None, public=True)
|
||||
|
||||
renderer = self.get_renderer(options['format'])
|
||||
output = renderer.render(schema, renderer_context={})
|
||||
self.stdout.write(output.decode('utf-8'))
|
||||
self.stdout.write(output.decode())
|
||||
|
||||
def get_renderer(self, format):
|
||||
renderer_cls = {
|
||||
'corejson': CoreJSONRenderer,
|
||||
'openapi': OpenAPIRenderer,
|
||||
'openapi-json': JSONOpenAPIRenderer,
|
||||
}[format]
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
renderer_cls = {
|
||||
'corejson': renderers.CoreJSONRenderer,
|
||||
'openapi': renderers.CoreAPIOpenAPIRenderer,
|
||||
'openapi-json': renderers.CoreAPIJSONOpenAPIRenderer,
|
||||
}[format]
|
||||
return renderer_cls()
|
||||
|
||||
renderer_cls = {
|
||||
'openapi': renderers.OpenAPIRenderer,
|
||||
'openapi-json': renderers.JSONOpenAPIRenderer,
|
||||
}[format]
|
||||
return renderer_cls()
|
||||
|
||||
def get_generator_class(self):
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
return coreapi.SchemaGenerator
|
||||
return SchemaGenerator
|
||||
|
|
|
@ -6,8 +6,6 @@ some fairly ad-hoc information about the view.
|
|||
Future implementations might use JSON schema or other definitions in order
|
||||
to return this information in a more standardized way.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
@ -19,7 +17,7 @@ from rest_framework.request import clone_request
|
|||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
||||
class BaseMetadata(object):
|
||||
class BaseMetadata:
|
||||
def determine_metadata(self, request, view):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
|
|
|
@ -4,14 +4,12 @@ Basic building blocks for generic class based views.
|
|||
We don't bind behaviour to http method handlers yet,
|
||||
which allows mixin classes to be composed in interesting ways.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
class CreateModelMixin(object):
|
||||
class CreateModelMixin:
|
||||
"""
|
||||
Create a model instance.
|
||||
"""
|
||||
|
@ -32,7 +30,7 @@ class CreateModelMixin(object):
|
|||
return {}
|
||||
|
||||
|
||||
class ListModelMixin(object):
|
||||
class ListModelMixin:
|
||||
"""
|
||||
List a queryset.
|
||||
"""
|
||||
|
@ -48,7 +46,7 @@ class ListModelMixin(object):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RetrieveModelMixin(object):
|
||||
class RetrieveModelMixin:
|
||||
"""
|
||||
Retrieve a model instance.
|
||||
"""
|
||||
|
@ -58,7 +56,7 @@ class RetrieveModelMixin(object):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UpdateModelMixin(object):
|
||||
class UpdateModelMixin:
|
||||
"""
|
||||
Update a model instance.
|
||||
"""
|
||||
|
@ -84,7 +82,7 @@ class UpdateModelMixin(object):
|
|||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DestroyModelMixin(object):
|
||||
class DestroyModelMixin:
|
||||
"""
|
||||
Destroy a model instance.
|
||||
"""
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
Content negotiation deals with selecting an appropriate renderer given the
|
||||
incoming request. Typically this will be based on the request's Accept header.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING, exceptions
|
||||
|
@ -13,7 +11,7 @@ from rest_framework.utils.mediatypes import (
|
|||
)
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
class BaseContentNegotiation:
|
||||
def select_parser(self, request, parsers):
|
||||
raise NotImplementedError('.select_parser() must be implemented')
|
||||
|
||||
|
@ -66,7 +64,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
# Accepted media type is 'application/json'
|
||||
full_media_type = ';'.join(
|
||||
(renderer.media_type,) +
|
||||
tuple('{0}={1}'.format(
|
||||
tuple('{}={}'.format(
|
||||
key, value.decode(HTTP_HEADER_ENCODING))
|
||||
for key, value in media_type_wrapper.params.items()))
|
||||
return renderer, full_media_type
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
# coding: utf-8
|
||||
"""
|
||||
Pagination serializers determine the structure of the output that should
|
||||
be used for paginated responses.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import OrderedDict, namedtuple
|
||||
from urllib import parse
|
||||
|
||||
from django.core.paginator import InvalidPage
|
||||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
@ -133,7 +129,7 @@ PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
|
|||
PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
|
||||
|
||||
|
||||
class BasePagination(object):
|
||||
class BasePagination:
|
||||
display_page_controls = False
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
|
||||
|
@ -152,6 +148,9 @@ class BasePagination(object):
|
|||
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
|
||||
return []
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class PageNumberPagination(BasePagination):
|
||||
"""
|
||||
|
@ -204,7 +203,7 @@ class PageNumberPagination(BasePagination):
|
|||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
msg = self.invalid_page_message.format(
|
||||
page_number=page_number, message=six.text_type(exc)
|
||||
page_number=page_number, message=str(exc)
|
||||
)
|
||||
raise NotFound(msg)
|
||||
|
||||
|
@ -305,6 +304,32 @@ class PageNumberPagination(BasePagination):
|
|||
)
|
||||
return fields
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
parameters = [
|
||||
{
|
||||
'name': self.page_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.page_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
]
|
||||
if self.page_size_query_param is not None:
|
||||
parameters.append(
|
||||
{
|
||||
'name': self.page_size_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.page_size_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
)
|
||||
return parameters
|
||||
|
||||
|
||||
class LimitOffsetPagination(BasePagination):
|
||||
"""
|
||||
|
@ -434,6 +459,15 @@ class LimitOffsetPagination(BasePagination):
|
|||
context = self.get_html_context()
|
||||
return template.render(context)
|
||||
|
||||
def get_count(self, queryset):
|
||||
"""
|
||||
Determine an object count, supporting either querysets or regular lists.
|
||||
"""
|
||||
try:
|
||||
return queryset.count()
|
||||
except (AttributeError, TypeError):
|
||||
return len(queryset)
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
|
||||
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
|
||||
|
@ -458,14 +492,28 @@ class LimitOffsetPagination(BasePagination):
|
|||
)
|
||||
]
|
||||
|
||||
def get_count(self, queryset):
|
||||
"""
|
||||
Determine an object count, supporting either querysets or regular lists.
|
||||
"""
|
||||
try:
|
||||
return queryset.count()
|
||||
except (AttributeError, TypeError):
|
||||
return len(queryset)
|
||||
def get_schema_operation_parameters(self, view):
|
||||
parameters = [
|
||||
{
|
||||
'name': self.limit_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.limit_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': self.offset_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.offset_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
},
|
||||
]
|
||||
return parameters
|
||||
|
||||
|
||||
class CursorPagination(BasePagination):
|
||||
|
@ -589,7 +637,7 @@ class CursorPagination(BasePagination):
|
|||
if not self.has_next:
|
||||
return None
|
||||
|
||||
if self.cursor and self.cursor.reverse and self.cursor.offset != 0:
|
||||
if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
|
||||
# If we're reversing direction and we have an offset cursor
|
||||
# then we cannot use the first position we find as a marker.
|
||||
compare = self._get_position_from_instance(self.page[-1], self.ordering)
|
||||
|
@ -597,12 +645,14 @@ class CursorPagination(BasePagination):
|
|||
compare = self.next_position
|
||||
offset = 0
|
||||
|
||||
has_item_with_unique_position = False
|
||||
for item in reversed(self.page):
|
||||
position = self._get_position_from_instance(item, self.ordering)
|
||||
if position != compare:
|
||||
# The item in this position and the item following it
|
||||
# have different positions. We can use this position as
|
||||
# our marker.
|
||||
has_item_with_unique_position = True
|
||||
break
|
||||
|
||||
# The item in this position has the same position as the item
|
||||
|
@ -611,7 +661,7 @@ class CursorPagination(BasePagination):
|
|||
compare = position
|
||||
offset += 1
|
||||
|
||||
else:
|
||||
if self.page and not has_item_with_unique_position:
|
||||
# There were no unique positions in the page.
|
||||
if not self.has_previous:
|
||||
# We are on the first page.
|
||||
|
@ -630,6 +680,9 @@ class CursorPagination(BasePagination):
|
|||
offset = self.cursor.offset + self.page_size
|
||||
position = self.previous_position
|
||||
|
||||
if not self.page:
|
||||
position = self.next_position
|
||||
|
||||
cursor = Cursor(offset=offset, reverse=False, position=position)
|
||||
return self.encode_cursor(cursor)
|
||||
|
||||
|
@ -637,7 +690,7 @@ class CursorPagination(BasePagination):
|
|||
if not self.has_previous:
|
||||
return None
|
||||
|
||||
if self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
|
||||
if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
|
||||
# If we're reversing direction and we have an offset cursor
|
||||
# then we cannot use the first position we find as a marker.
|
||||
compare = self._get_position_from_instance(self.page[0], self.ordering)
|
||||
|
@ -645,12 +698,14 @@ class CursorPagination(BasePagination):
|
|||
compare = self.previous_position
|
||||
offset = 0
|
||||
|
||||
has_item_with_unique_position = False
|
||||
for item in self.page:
|
||||
position = self._get_position_from_instance(item, self.ordering)
|
||||
if position != compare:
|
||||
# The item in this position and the item following it
|
||||
# have different positions. We can use this position as
|
||||
# our marker.
|
||||
has_item_with_unique_position = True
|
||||
break
|
||||
|
||||
# The item in this position has the same position as the item
|
||||
|
@ -659,7 +714,7 @@ class CursorPagination(BasePagination):
|
|||
compare = position
|
||||
offset += 1
|
||||
|
||||
else:
|
||||
if self.page and not has_item_with_unique_position:
|
||||
# There were no unique positions in the page.
|
||||
if not self.has_next:
|
||||
# We are on the final page.
|
||||
|
@ -678,6 +733,9 @@ class CursorPagination(BasePagination):
|
|||
offset = 0
|
||||
position = self.next_position
|
||||
|
||||
if not self.page:
|
||||
position = self.previous_position
|
||||
|
||||
cursor = Cursor(offset=offset, reverse=True, position=position)
|
||||
return self.encode_cursor(cursor)
|
||||
|
||||
|
@ -716,13 +774,13 @@ class CursorPagination(BasePagination):
|
|||
'nearly-unique field on the model, such as "-created" or "pk".'
|
||||
)
|
||||
|
||||
assert isinstance(ordering, (six.string_types, list, tuple)), (
|
||||
assert isinstance(ordering, (str, list, tuple)), (
|
||||
'Invalid ordering. Expected string or tuple, but got {type}'.format(
|
||||
type=type(ordering).__name__
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(ordering, six.string_types):
|
||||
if isinstance(ordering, str):
|
||||
return (ordering,)
|
||||
return tuple(ordering)
|
||||
|
||||
|
@ -737,7 +795,7 @@ class CursorPagination(BasePagination):
|
|||
|
||||
try:
|
||||
querystring = b64decode(encoded.encode('ascii')).decode('ascii')
|
||||
tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
|
||||
tokens = parse.parse_qs(querystring, keep_blank_values=True)
|
||||
|
||||
offset = tokens.get('o', ['0'])[0]
|
||||
offset = _positive_int(offset, cutoff=self.offset_cutoff)
|
||||
|
@ -763,7 +821,7 @@ class CursorPagination(BasePagination):
|
|||
if cursor.position is not None:
|
||||
tokens['p'] = cursor.position
|
||||
|
||||
querystring = urlparse.urlencode(tokens, doseq=True)
|
||||
querystring = parse.urlencode(tokens, doseq=True)
|
||||
encoded = b64encode(querystring.encode('ascii')).decode('ascii')
|
||||
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
|
||||
|
||||
|
@ -773,7 +831,7 @@ class CursorPagination(BasePagination):
|
|||
attr = instance[field_name]
|
||||
else:
|
||||
attr = getattr(instance, field_name)
|
||||
return six.text_type(attr)
|
||||
return str(attr)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
|
@ -820,3 +878,29 @@ class CursorPagination(BasePagination):
|
|||
)
|
||||
)
|
||||
return fields
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
parameters = [
|
||||
{
|
||||
'name': self.cursor_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.cursor_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
}
|
||||
]
|
||||
if self.page_size_query_param is not None:
|
||||
parameters.append(
|
||||
{
|
||||
'name': self.page_size_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.page_size_query_description),
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
},
|
||||
}
|
||||
)
|
||||
return parameters
|
||||
|
|
|
@ -4,9 +4,8 @@ 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.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import codecs
|
||||
from urllib import parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadhandler import StopFutureHandlers
|
||||
|
@ -15,9 +14,7 @@ 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 import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from rest_framework import renderers
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
@ -25,13 +22,13 @@ from rest_framework.settings import api_settings
|
|||
from rest_framework.utils import json
|
||||
|
||||
|
||||
class DataAndFiles(object):
|
||||
class DataAndFiles:
|
||||
def __init__(self, data, files):
|
||||
self.data = data
|
||||
self.files = files
|
||||
|
||||
|
||||
class BaseParser(object):
|
||||
class BaseParser:
|
||||
"""
|
||||
All parsers should extend `BaseParser`, specifying a `media_type`
|
||||
attribute, and overriding the `.parse()` method.
|
||||
|
@ -67,7 +64,7 @@ class JSONParser(BaseParser):
|
|||
parse_constant = json.strict_constant if self.strict else None
|
||||
return json.load(decoded_stream, parse_constant=parse_constant)
|
||||
except ValueError as exc:
|
||||
raise ParseError('JSON parse error - %s' % six.text_type(exc))
|
||||
raise ParseError('JSON parse error - %s' % str(exc))
|
||||
|
||||
|
||||
class FormParser(BaseParser):
|
||||
|
@ -83,8 +80,7 @@ class FormParser(BaseParser):
|
|||
"""
|
||||
parser_context = parser_context or {}
|
||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||
data = QueryDict(stream.read(), encoding=encoding)
|
||||
return data
|
||||
return QueryDict(stream.read(), encoding=encoding)
|
||||
|
||||
|
||||
class MultiPartParser(BaseParser):
|
||||
|
@ -113,7 +109,7 @@ class MultiPartParser(BaseParser):
|
|||
data, files = parser.parse()
|
||||
return DataAndFiles(data, files)
|
||||
except MultiPartParserError as exc:
|
||||
raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
|
||||
raise ParseError('Multipart form parse error - %s' % str(exc))
|
||||
|
||||
|
||||
class FileUploadParser(BaseParser):
|
||||
|
@ -205,7 +201,7 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
try:
|
||||
meta = parser_context['request'].META
|
||||
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
|
||||
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode())
|
||||
filename_parm = disposition[1]
|
||||
if 'filename*' in filename_parm:
|
||||
return self.get_encoded_filename(filename_parm)
|
||||
|
@ -221,7 +217,7 @@ class FileUploadParser(BaseParser):
|
|||
encoded_filename = force_text(filename_parm['filename*'])
|
||||
try:
|
||||
charset, lang, filename = encoded_filename.split('\'', 2)
|
||||
filename = urlparse.unquote(filename)
|
||||
filename = parse.unquote(filename)
|
||||
except (ValueError, LookupError):
|
||||
filename = force_text(filename_parm['filename'])
|
||||
return filename
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
"""
|
||||
Provides a set of pluggable permission policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http import Http404
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import exceptions
|
||||
|
||||
|
@ -101,8 +98,7 @@ class BasePermissionMetaclass(OperationHolderMixin, type):
|
|||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(BasePermissionMetaclass)
|
||||
class BasePermission(object):
|
||||
class BasePermission(metaclass=BasePermissionMetaclass):
|
||||
"""
|
||||
A base class from which all permission classes should inherit.
|
||||
"""
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from urllib import parse
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.db.models import Manager
|
||||
from django.db.models.query import QuerySet
|
||||
from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve
|
||||
from django.utils import six
|
||||
from django.utils.encoding import (
|
||||
python_2_unicode_compatible, smart_text, uri_to_iri
|
||||
)
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import smart_text, uri_to_iri
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.fields import (
|
||||
Field, empty, get_attribute, is_simple_callable, iter_options
|
||||
|
@ -46,14 +40,14 @@ class ObjectTypeError(TypeError):
|
|||
"""
|
||||
|
||||
|
||||
class Hyperlink(six.text_type):
|
||||
class Hyperlink(str):
|
||||
"""
|
||||
A string like object that additionally has an associated name.
|
||||
We use this for hyperlinked URLs that may render as a named link
|
||||
in some contexts, or render as a plain URL in others.
|
||||
"""
|
||||
def __new__(self, url, obj):
|
||||
ret = six.text_type.__new__(self, url)
|
||||
ret = str.__new__(self, url)
|
||||
ret.obj = obj
|
||||
return ret
|
||||
|
||||
|
@ -65,13 +59,12 @@ class Hyperlink(six.text_type):
|
|||
# This ensures that we only called `__str__` lazily,
|
||||
# as in some cases calling __str__ on a model instances *might*
|
||||
# involve a database lookup.
|
||||
return six.text_type(self.obj)
|
||||
return str(self.obj)
|
||||
|
||||
is_hyperlink = True
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PKOnlyObject(object):
|
||||
class PKOnlyObject:
|
||||
"""
|
||||
This is a mock object, used for when we only need the pk of the object
|
||||
instance, but still want to return an object with a .pk attribute,
|
||||
|
@ -121,14 +114,14 @@ class RelatedField(Field):
|
|||
)
|
||||
kwargs.pop('many', None)
|
||||
kwargs.pop('allow_empty', None)
|
||||
super(RelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automagically create
|
||||
# `ManyRelatedField` classes instead when `many=True` is set.
|
||||
if kwargs.pop('many', False):
|
||||
return cls.many_init(*args, **kwargs)
|
||||
return super(RelatedField, cls).__new__(cls, *args, **kwargs)
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
|
@ -157,7 +150,7 @@ class RelatedField(Field):
|
|||
# We force empty strings to None values for relational fields.
|
||||
if data == '':
|
||||
data = None
|
||||
return super(RelatedField, self).run_validation(data)
|
||||
return super().run_validation(data)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
|
@ -189,7 +182,7 @@ class RelatedField(Field):
|
|||
pass
|
||||
|
||||
# Standard case, return the object instance.
|
||||
return super(RelatedField, self).get_attribute(instance)
|
||||
return super().get_attribute(instance)
|
||||
|
||||
def get_choices(self, cutoff=None):
|
||||
queryset = self.get_queryset()
|
||||
|
@ -225,7 +218,7 @@ class RelatedField(Field):
|
|||
)
|
||||
|
||||
def display_value(self, instance):
|
||||
return six.text_type(instance)
|
||||
return str(instance)
|
||||
|
||||
|
||||
class StringRelatedField(RelatedField):
|
||||
|
@ -236,10 +229,10 @@ class StringRelatedField(RelatedField):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['read_only'] = True
|
||||
super(StringRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return six.text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
|
@ -251,7 +244,7 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
self.pk_field = kwargs.pop('pk_field', None)
|
||||
super(PrimaryKeyRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return True
|
||||
|
@ -297,7 +290,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
# implicit `self` argument to be passed.
|
||||
self.reverse = reverse
|
||||
|
||||
super(HyperlinkedRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.lookup_field == 'pk'
|
||||
|
@ -317,10 +310,10 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
return queryset.get(**lookup_kwargs)
|
||||
except ValueError:
|
||||
exc = ObjectValueError(str(sys.exc_info()[1]))
|
||||
six.reraise(type(exc), exc, sys.exc_info()[2])
|
||||
raise exc.with_traceback(sys.exc_info()[2])
|
||||
except TypeError:
|
||||
exc = ObjectTypeError(str(sys.exc_info()[1]))
|
||||
six.reraise(type(exc), exc, sys.exc_info()[2])
|
||||
raise exc.with_traceback(sys.exc_info()[2])
|
||||
|
||||
def get_url(self, obj, view_name, request, format):
|
||||
"""
|
||||
|
@ -346,7 +339,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
if http_prefix:
|
||||
# If needed convert absolute URLs to relative path
|
||||
data = urlparse.urlparse(data).path
|
||||
data = parse.urlparse(data).path
|
||||
prefix = get_script_prefix()
|
||||
if data.startswith(prefix):
|
||||
data = '/' + data[len(prefix):]
|
||||
|
@ -432,7 +425,7 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
|
|||
assert view_name is not None, 'The `view_name` argument is required.'
|
||||
kwargs['read_only'] = True
|
||||
kwargs['source'] = '*'
|
||||
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
|
||||
super().__init__(view_name, **kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
# We have the complete object instance already. We don't need
|
||||
|
@ -453,7 +446,7 @@ class SlugRelatedField(RelatedField):
|
|||
def __init__(self, slug_field=None, **kwargs):
|
||||
assert slug_field is not None, 'The `slug_field` argument is required.'
|
||||
self.slug_field = slug_field
|
||||
super(SlugRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -502,7 +495,7 @@ class ManyRelatedField(Field):
|
|||
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
||||
)
|
||||
assert child_relation is not None, '`child_relation` is a required argument.'
|
||||
super(ManyRelatedField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child_relation.bind(field_name='', parent=self)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -518,7 +511,7 @@ class ManyRelatedField(Field):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
|
|
@ -6,10 +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.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from urllib import parse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -19,9 +18,7 @@ from django.http.multipartparser import parse_header
|
|||
from django.template import engines, loader
|
||||
from django.test.client import encode_multipart
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils import six
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from rest_framework import VERSION, exceptions, serializers, status
|
||||
from rest_framework.compat import (
|
||||
|
@ -40,7 +37,7 @@ def zero_as_none(value):
|
|||
return None if value == 0 else value
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
class BaseRenderer:
|
||||
"""
|
||||
All renderers should extend this class, setting the `media_type`
|
||||
and `format` attributes, and override the `.render()` method.
|
||||
|
@ -91,7 +88,7 @@ class JSONRenderer(BaseRenderer):
|
|||
Render `data` into JSON, returning a bytestring.
|
||||
"""
|
||||
if data is None:
|
||||
return bytes()
|
||||
return b''
|
||||
|
||||
renderer_context = renderer_context or {}
|
||||
indent = self.get_indent(accepted_media_type, renderer_context)
|
||||
|
@ -107,18 +104,11 @@ class JSONRenderer(BaseRenderer):
|
|||
allow_nan=not self.strict, separators=separators
|
||||
)
|
||||
|
||||
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||
# but if ensure_ascii=False, the return type is underspecified,
|
||||
# and may (or may not) be unicode.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(ret, six.text_type):
|
||||
# We always fully escape \u2028 and \u2029 to ensure we output JSON
|
||||
# that is a strict javascript subset. If bytes were returned
|
||||
# by json.dumps() then we don't have these characters in any case.
|
||||
# See: http://timelessrepo.com/json-isnt-a-javascript-subset
|
||||
ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
|
||||
return bytes(ret.encode('utf-8'))
|
||||
return ret
|
||||
# We always fully escape \u2028 and \u2029 to ensure we output JSON
|
||||
# that is a strict javascript subset.
|
||||
# See: http://timelessrepo.com/json-isnt-a-javascript-subset
|
||||
ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
|
||||
return ret.encode()
|
||||
|
||||
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
|
@ -349,7 +339,7 @@ class HTMLFormRenderer(BaseRenderer):
|
|||
# Get a clone of the field with text-only value representation.
|
||||
field = field.as_form_field()
|
||||
|
||||
if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type):
|
||||
if style.get('input_type') == 'datetime-local' and isinstance(field.value, str):
|
||||
field.value = field.value.rstrip('Z')
|
||||
|
||||
if 'template' in style:
|
||||
|
@ -577,7 +567,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
data.pop(name, None)
|
||||
content = renderer.render(data, accepted, context)
|
||||
# Renders returns bytes, but CharField expects a str.
|
||||
content = content.decode('utf-8')
|
||||
content = content.decode()
|
||||
else:
|
||||
content = None
|
||||
|
||||
|
@ -684,7 +674,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
csrf_header_name = csrf_header_name[5:]
|
||||
csrf_header_name = csrf_header_name.replace('_', '-')
|
||||
|
||||
context = {
|
||||
return {
|
||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
'code_style': pygments_css(self.code_style),
|
||||
'view': view,
|
||||
|
@ -720,7 +710,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'csrf_cookie_name': csrf_cookie_name,
|
||||
'csrf_header_name': csrf_header_name
|
||||
}
|
||||
return context
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -791,7 +780,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
|||
"""
|
||||
Render the HTML for the browsable API representation.
|
||||
"""
|
||||
context = super(AdminRenderer, self).get_context(
|
||||
context = super().get_context(
|
||||
data, accepted_media_type, renderer_context
|
||||
)
|
||||
|
||||
|
@ -995,14 +984,14 @@ class _BaseOpenAPIRenderer:
|
|||
|
||||
tag = None
|
||||
for name, link in document.links.items():
|
||||
path = urlparse.urlparse(link.url).path
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||
|
||||
for tag, section in document.data.items():
|
||||
for name, link in section.links.items():
|
||||
path = urlparse.urlparse(link.url).path
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||
|
@ -1024,28 +1013,49 @@ class _BaseOpenAPIRenderer:
|
|||
}
|
||||
|
||||
|
||||
class OpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
media_type = 'application/vnd.oai.openapi'
|
||||
charset = None
|
||||
format = 'openapi'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
structure = self.get_structure(data)
|
||||
return yaml.dump(structure, default_flow_style=False).encode('utf-8')
|
||||
return yaml.dump(structure, default_flow_style=False).encode()
|
||||
|
||||
|
||||
class JSONOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
media_type = 'application/vnd.oai.openapi+json'
|
||||
charset = None
|
||||
format = 'openapi-json'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
structure = self.get_structure(data)
|
||||
return json.dumps(structure, indent=4).encode('utf-8')
|
||||
|
||||
|
||||
class OpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi'
|
||||
charset = None
|
||||
format = 'openapi'
|
||||
|
||||
def __init__(self):
|
||||
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return yaml.dump(data, default_flow_style=False, sort_keys=False).encode('utf-8')
|
||||
|
||||
|
||||
class JSONOpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi+json'
|
||||
charset = None
|
||||
format = 'openapi-json'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return json.dumps(data, indent=2).encode('utf-8')
|
||||
|
|
|
@ -8,8 +8,6 @@ The wrapped request then offers a richer API, in particular :
|
|||
- full support of PUT method, including support for file uploads
|
||||
- form overloading of HTTP method, content type and content
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
@ -18,7 +16,6 @@ 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 import six
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING, exceptions
|
||||
|
@ -34,7 +31,7 @@ def is_form_media_type(media_type):
|
|||
base_media_type == 'multipart/form-data')
|
||||
|
||||
|
||||
class override_method(object):
|
||||
class override_method:
|
||||
"""
|
||||
A context manager that temporarily overrides the method on a request,
|
||||
additionally setting the `view.request` attribute.
|
||||
|
@ -78,10 +75,10 @@ def wrap_attributeerrors():
|
|||
except AttributeError:
|
||||
info = sys.exc_info()
|
||||
exc = WrappedAttributeError(str(info[1]))
|
||||
six.reraise(type(exc), exc, info[2])
|
||||
raise exc.with_traceback(info[2])
|
||||
|
||||
|
||||
class Empty(object):
|
||||
class Empty:
|
||||
"""
|
||||
Placeholder for unset attributes.
|
||||
Cannot use `None`, as that may be a valid value.
|
||||
|
@ -126,7 +123,7 @@ def clone_request(request, method):
|
|||
return ret
|
||||
|
||||
|
||||
class ForcedAuthentication(object):
|
||||
class ForcedAuthentication:
|
||||
"""
|
||||
This authentication class is used if the test client or request factory
|
||||
forcibly authenticated the request.
|
||||
|
@ -140,7 +137,7 @@ class ForcedAuthentication(object):
|
|||
return (self.force_user, self.force_token)
|
||||
|
||||
|
||||
class Request(object):
|
||||
class Request:
|
||||
"""
|
||||
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||
|
||||
|
|
|
@ -4,11 +4,9 @@ it is initialized with unrendered data, instead of a pre-rendered string.
|
|||
|
||||
The appropriate renderer is called during Django's template response rendering.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from http.client import responses
|
||||
|
||||
from django.template.response import SimpleTemplateResponse
|
||||
from django.utils import six
|
||||
from django.utils.six.moves.http_client import responses
|
||||
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
|
@ -29,7 +27,7 @@ class Response(SimpleTemplateResponse):
|
|||
Setting 'renderer' and 'media_type' will typically be deferred,
|
||||
For example being set automatically by the `APIView`.
|
||||
"""
|
||||
super(Response, self).__init__(None, status=status)
|
||||
super().__init__(None, status=status)
|
||||
|
||||
if isinstance(data, Serializer):
|
||||
msg = (
|
||||
|
@ -45,7 +43,7 @@ class Response(SimpleTemplateResponse):
|
|||
self.content_type = content_type
|
||||
|
||||
if headers:
|
||||
for name, value in six.iteritems(headers):
|
||||
for name, value in headers.items():
|
||||
self[name] = value
|
||||
|
||||
@property
|
||||
|
@ -64,18 +62,18 @@ class Response(SimpleTemplateResponse):
|
|||
content_type = self.content_type
|
||||
|
||||
if content_type is None and charset is not None:
|
||||
content_type = "{0}; charset={1}".format(media_type, charset)
|
||||
content_type = "{}; charset={}".format(media_type, charset)
|
||||
elif content_type is None:
|
||||
content_type = media_type
|
||||
self['Content-Type'] = content_type
|
||||
|
||||
ret = renderer.render(self.data, accepted_media_type, context)
|
||||
if isinstance(ret, six.text_type):
|
||||
if isinstance(ret, str):
|
||||
assert charset, (
|
||||
'renderer returned unicode, and did not specify '
|
||||
'a charset value.'
|
||||
)
|
||||
return bytes(ret.encode(charset))
|
||||
return ret.encode(charset)
|
||||
|
||||
if not ret:
|
||||
del self['Content-Type']
|
||||
|
@ -94,7 +92,7 @@ class Response(SimpleTemplateResponse):
|
|||
"""
|
||||
Remove attributes from the response that shouldn't be cached.
|
||||
"""
|
||||
state = super(Response, self).__getstate__()
|
||||
state = super().__getstate__()
|
||||
for key in (
|
||||
'accepted_renderer', 'renderer_context', 'resolver_match',
|
||||
'client', 'request', 'json', 'wsgi_request'
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
"""
|
||||
Provide urlresolver functions that return fully qualified URLs or view names
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import NoReverseMatch
|
||||
from django.urls import reverse as django_reverse
|
||||
from django.utils import six
|
||||
from django.utils.functional import lazy
|
||||
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -66,4 +63,4 @@ def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extr
|
|||
return url
|
||||
|
||||
|
||||
reverse_lazy = lazy(reverse, six.text_type)
|
||||
reverse_lazy = lazy(reverse, str)
|
||||
|
|
|
@ -13,8 +13,6 @@ For example, you might have a `urls.py` that looks something like this:
|
|||
|
||||
urlpatterns = router.urls
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import warnings
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
@ -22,12 +20,9 @@ from collections import OrderedDict, namedtuple
|
|||
from django.conf.urls import url
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils import six
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
|
||||
from rest_framework import (
|
||||
RemovedInDRF310Warning, RemovedInDRF311Warning, views
|
||||
)
|
||||
from rest_framework import RemovedInDRF311Warning, views
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
|
@ -39,28 +34,6 @@ Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs'])
|
|||
DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'])
|
||||
|
||||
|
||||
class DynamicDetailRoute(object):
|
||||
def __new__(cls, url, name, initkwargs):
|
||||
warnings.warn(
|
||||
"`DynamicDetailRoute` is deprecated and will be removed in 3.10 "
|
||||
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||
"`DynamicRoute(url, name, True, initkwargs)` instead.",
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
return DynamicRoute(url, name, True, initkwargs)
|
||||
|
||||
|
||||
class DynamicListRoute(object):
|
||||
def __new__(cls, url, name, initkwargs):
|
||||
warnings.warn(
|
||||
"`DynamicListRoute` is deprecated and will be removed in 3.10 in "
|
||||
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||
"`DynamicRoute(url, name, False, initkwargs)` instead.",
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
return DynamicRoute(url, name, False, initkwargs)
|
||||
|
||||
|
||||
def escape_curly_brackets(url_path):
|
||||
"""
|
||||
Double brackets in regex of url_path for escape string formatting
|
||||
|
@ -83,7 +56,7 @@ class RenameRouterMethods(RenameMethodsBase):
|
|||
)
|
||||
|
||||
|
||||
class BaseRouter(six.with_metaclass(RenameRouterMethods)):
|
||||
class BaseRouter(metaclass=RenameRouterMethods):
|
||||
def __init__(self):
|
||||
self.registry = []
|
||||
|
||||
|
@ -173,7 +146,7 @@ class SimpleRouter(BaseRouter):
|
|||
|
||||
def __init__(self, trailing_slash=True):
|
||||
self.trailing_slash = '/' if trailing_slash else ''
|
||||
super(SimpleRouter, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
def get_default_basename(self, viewset):
|
||||
"""
|
||||
|
@ -365,7 +338,7 @@ class DefaultRouter(SimpleRouter):
|
|||
self.root_renderers = kwargs.pop('root_renderers')
|
||||
else:
|
||||
self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)
|
||||
super(DefaultRouter, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_api_root_view(self, api_urls=None):
|
||||
"""
|
||||
|
@ -383,7 +356,7 @@ class DefaultRouter(SimpleRouter):
|
|||
Generate the list of URL patterns, including a default root view
|
||||
for the API, and appending `.json` style format suffixes.
|
||||
"""
|
||||
urls = super(DefaultRouter, self).get_urls()
|
||||
urls = super().get_urls()
|
||||
|
||||
if self.include_root_view:
|
||||
view = self.get_api_root_view(api_urls=urls)
|
||||
|
|
|
@ -22,24 +22,32 @@ Other access should target the submodules directly
|
|||
"""
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from .generators import SchemaGenerator
|
||||
from .inspectors import AutoSchema, DefaultSchema, ManualSchema # noqa
|
||||
from . import coreapi, openapi
|
||||
from .inspectors import DefaultSchema # noqa
|
||||
from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa
|
||||
|
||||
|
||||
def get_schema_view(
|
||||
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
|
||||
public=False, patterns=None, generator_class=SchemaGenerator,
|
||||
public=False, patterns=None, generator_class=None,
|
||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
||||
"""
|
||||
Return a schema view.
|
||||
"""
|
||||
# Avoid import cycle on APIView
|
||||
from .views import SchemaView
|
||||
if generator_class is None:
|
||||
if coreapi.is_enabled():
|
||||
generator_class = coreapi.SchemaGenerator
|
||||
else:
|
||||
generator_class = openapi.SchemaGenerator
|
||||
|
||||
generator = generator_class(
|
||||
title=title, url=url, description=description,
|
||||
urlconf=urlconf, patterns=patterns,
|
||||
)
|
||||
|
||||
# Avoid import cycle on APIView
|
||||
from .views import SchemaView
|
||||
return SchemaView.as_view(
|
||||
renderer_classes=renderer_classes,
|
||||
schema_generator=generator,
|
||||
|
|
616
rest_framework/schemas/coreapi.py
Normal file
|
@ -0,0 +1,616 @@
|
|||
import re
|
||||
import warnings
|
||||
from collections import Counter, OrderedDict
|
||||
from urllib import parse
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import coreapi, coreschema, uritemplate
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import formatting
|
||||
|
||||
from .generators import BaseSchemaGenerator
|
||||
from .inspectors import ViewInspector
|
||||
from .utils import get_pk_description, is_list_view
|
||||
|
||||
# Used in _get_description_section()
|
||||
# TODO: ???: move up to base.
|
||||
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
|
||||
|
||||
# Generator #
|
||||
# TODO: Pull some of this into base.
|
||||
|
||||
|
||||
def is_custom_action(action):
|
||||
return action not in {
|
||||
'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy'
|
||||
}
|
||||
|
||||
|
||||
def distribute_links(obj):
|
||||
for key, value in obj.items():
|
||||
distribute_links(value)
|
||||
|
||||
for preferred_key, link in obj.links:
|
||||
key = obj.get_available_key(preferred_key)
|
||||
obj[key] = link
|
||||
|
||||
|
||||
INSERT_INTO_COLLISION_FMT = """
|
||||
Schema Naming Collision.
|
||||
|
||||
coreapi.Link for URL path {value_url} cannot be inserted into schema.
|
||||
Position conflicts with coreapi.Link for URL path {target_url}.
|
||||
|
||||
Attempted to insert link with keys: {keys}.
|
||||
|
||||
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
|
||||
to customise schema structure.
|
||||
"""
|
||||
|
||||
|
||||
class LinkNode(OrderedDict):
|
||||
def __init__(self):
|
||||
self.links = []
|
||||
self.methods_counter = Counter()
|
||||
super(LinkNode, self).__init__()
|
||||
|
||||
def get_available_key(self, preferred_key):
|
||||
if preferred_key not in self:
|
||||
return preferred_key
|
||||
|
||||
while True:
|
||||
current_val = self.methods_counter[preferred_key]
|
||||
self.methods_counter[preferred_key] += 1
|
||||
|
||||
key = '{}_{}'.format(preferred_key, current_val)
|
||||
if key not in self:
|
||||
return key
|
||||
|
||||
|
||||
def insert_into(target, keys, value):
|
||||
"""
|
||||
Nested dictionary insertion.
|
||||
|
||||
>>> example = {}
|
||||
>>> insert_into(example, ['a', 'b', 'c'], 123)
|
||||
>>> example
|
||||
LinkNode({'a': LinkNode({'b': LinkNode({'c': LinkNode(links=[123])}}})))
|
||||
"""
|
||||
for key in keys[:-1]:
|
||||
if key not in target:
|
||||
target[key] = LinkNode()
|
||||
target = target[key]
|
||||
|
||||
try:
|
||||
target.links.append((keys[-1], value))
|
||||
except TypeError:
|
||||
msg = INSERT_INTO_COLLISION_FMT.format(
|
||||
value_url=value.url,
|
||||
target_url=target.url,
|
||||
keys=keys
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
class SchemaGenerator(BaseSchemaGenerator):
|
||||
"""
|
||||
Original CoreAPI version.
|
||||
"""
|
||||
# Map HTTP methods onto actions.
|
||||
default_mapping = {
|
||||
'get': 'retrieve',
|
||||
'post': 'create',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
'delete': 'destroy',
|
||||
}
|
||||
|
||||
# Map the method names we use for viewset actions onto external schema names.
|
||||
# These give us names that are more suitable for the external representation.
|
||||
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
|
||||
coerce_method_names = None
|
||||
|
||||
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
|
||||
assert coreapi, '`coreapi` must be installed for schema support.'
|
||||
assert coreschema, '`coreschema` must be installed for schema support.'
|
||||
|
||||
super(SchemaGenerator, self).__init__(title, url, description, patterns, urlconf)
|
||||
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||
|
||||
def get_links(self, request=None):
|
||||
"""
|
||||
Return a dictionary containing all the links that should be
|
||||
included in the API schema.
|
||||
"""
|
||||
links = LinkNode()
|
||||
|
||||
paths, view_endpoints = self._get_paths_and_endpoints(request)
|
||||
|
||||
# Only generate the path prefix for paths that will be included
|
||||
if not paths:
|
||||
return None
|
||||
prefix = self.determine_path_prefix(paths)
|
||||
|
||||
for path, method, view in view_endpoints:
|
||||
if not self.has_view_permissions(path, method, view):
|
||||
continue
|
||||
link = view.schema.get_link(path, method, base_url=self.url)
|
||||
subpath = path[len(prefix):]
|
||||
keys = self.get_keys(subpath, method, view)
|
||||
insert_into(links, keys, link)
|
||||
|
||||
return links
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""
|
||||
Generate a `coreapi.Document` representing the API schema.
|
||||
"""
|
||||
self._initialise_endpoints()
|
||||
|
||||
links = self.get_links(None if public else request)
|
||||
if not links:
|
||||
return None
|
||||
|
||||
url = self.url
|
||||
if not url and request is not None:
|
||||
url = request.build_absolute_uri()
|
||||
|
||||
distribute_links(links)
|
||||
return coreapi.Document(
|
||||
title=self.title, description=self.description,
|
||||
url=url, content=links
|
||||
)
|
||||
|
||||
# Method for generating the link layout....
|
||||
def get_keys(self, subpath, method, view):
|
||||
"""
|
||||
Return a list of keys that should be used to layout a link within
|
||||
the schema document.
|
||||
|
||||
/users/ ("users", "list"), ("users", "create")
|
||||
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
|
||||
/users/enabled/ ("users", "enabled") # custom viewset list action
|
||||
/users/{pk}/star/ ("users", "star") # custom viewset detail action
|
||||
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
|
||||
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
|
||||
"""
|
||||
if hasattr(view, 'action'):
|
||||
# Viewsets have explicitly named actions.
|
||||
action = view.action
|
||||
else:
|
||||
# Views have no associated action, so we determine one from the method.
|
||||
if is_list_view(subpath, method, view):
|
||||
action = 'list'
|
||||
else:
|
||||
action = self.default_mapping[method.lower()]
|
||||
|
||||
named_path_components = [
|
||||
component for component
|
||||
in subpath.strip('/').split('/')
|
||||
if '{' not in component
|
||||
]
|
||||
|
||||
if is_custom_action(action):
|
||||
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
|
||||
if len(view.action_map) > 1:
|
||||
action = self.default_mapping[method.lower()]
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
return named_path_components + [action]
|
||||
else:
|
||||
return named_path_components[:-1] + [action]
|
||||
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
|
||||
# Default action, eg "/users/", "/users/{pk}/"
|
||||
return named_path_components + [action]
|
||||
|
||||
# View Inspectors #
|
||||
|
||||
|
||||
def field_to_schema(field):
|
||||
title = force_text(field.label) if field.label else ''
|
||||
description = force_text(field.help_text) if field.help_text else ''
|
||||
|
||||
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
|
||||
child_schema = field_to_schema(field.child)
|
||||
return coreschema.Array(
|
||||
items=child_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.DictField):
|
||||
return coreschema.Object(
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.Serializer):
|
||||
return coreschema.Object(
|
||||
properties=OrderedDict([
|
||||
(key, field_to_schema(value))
|
||||
for key, value
|
||||
in field.fields.items()
|
||||
]),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ManyRelatedField):
|
||||
related_field_schema = field_to_schema(field.child_relation)
|
||||
|
||||
return coreschema.Array(
|
||||
items=related_field_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
schema_cls = coreschema.String
|
||||
model = getattr(field.queryset, 'model', None)
|
||||
if model is not None:
|
||||
model_field = model._meta.pk
|
||||
if isinstance(model_field, models.AutoField):
|
||||
schema_cls = coreschema.Integer
|
||||
return schema_cls(title=title, description=description)
|
||||
elif isinstance(field, serializers.RelatedField):
|
||||
return coreschema.String(title=title, description=description)
|
||||
elif isinstance(field, serializers.MultipleChoiceField):
|
||||
return coreschema.Array(
|
||||
items=coreschema.Enum(enum=list(field.choices)),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
return coreschema.Enum(
|
||||
enum=list(field.choices),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
return coreschema.Boolean(title=title, description=description)
|
||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||
return coreschema.Number(title=title, description=description)
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
return coreschema.Integer(title=title, description=description)
|
||||
elif isinstance(field, serializers.DateField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date'
|
||||
)
|
||||
elif isinstance(field, serializers.DateTimeField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date-time'
|
||||
)
|
||||
elif isinstance(field, serializers.JSONField):
|
||||
return coreschema.Object(title=title, description=description)
|
||||
|
||||
if field.style.get('base_template') == 'textarea.html':
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='textarea'
|
||||
)
|
||||
|
||||
return coreschema.String(title=title, description=description)
|
||||
|
||||
|
||||
class AutoSchema(ViewInspector):
|
||||
"""
|
||||
Default inspector for APIView
|
||||
|
||||
Responsible for per-view introspection and schema generation.
|
||||
"""
|
||||
def __init__(self, manual_fields=None):
|
||||
"""
|
||||
Parameters:
|
||||
|
||||
* `manual_fields`: list of `coreapi.Field` instances that
|
||||
will be added to auto-generated fields, overwriting on `Field.name`
|
||||
"""
|
||||
super(AutoSchema, self).__init__()
|
||||
if manual_fields is None:
|
||||
manual_fields = []
|
||||
self._manual_fields = manual_fields
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
"""
|
||||
Generate `coreapi.Link` for self.view, path and method.
|
||||
|
||||
This is the main _public_ access point.
|
||||
|
||||
Parameters:
|
||||
|
||||
* path: Route path for view from URLConf.
|
||||
* method: The HTTP request method.
|
||||
* base_url: The project "mount point" as given to SchemaGenerator
|
||||
"""
|
||||
fields = self.get_path_fields(path, method)
|
||||
fields += self.get_serializer_fields(path, method)
|
||||
fields += self.get_pagination_fields(path, method)
|
||||
fields += self.get_filter_fields(path, method)
|
||||
|
||||
manual_fields = self.get_manual_fields(path, method)
|
||||
fields = self.update_fields(fields, manual_fields)
|
||||
|
||||
if fields and any([field.location in ('form', 'body') for field in fields]):
|
||||
encoding = self.get_encoding(path, method)
|
||||
else:
|
||||
encoding = None
|
||||
|
||||
description = self.get_description(path, method)
|
||||
|
||||
if base_url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=parse.urljoin(base_url, path),
|
||||
action=method.lower(),
|
||||
encoding=encoding,
|
||||
fields=fields,
|
||||
description=description
|
||||
)
|
||||
|
||||
def get_description(self, path, method):
|
||||
"""
|
||||
Determine a link description.
|
||||
|
||||
This will be based on the method docstring if one exists,
|
||||
or else the class docstring.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
method_name = getattr(view, 'action', method.lower())
|
||||
method_docstring = getattr(view, method_name, None).__doc__
|
||||
if method_docstring:
|
||||
# An explicit docstring on the method or action.
|
||||
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
|
||||
else:
|
||||
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
|
||||
|
||||
def _get_description_section(self, view, header, description):
|
||||
lines = [line for line in description.splitlines()]
|
||||
current_section = ''
|
||||
sections = {'': ''}
|
||||
|
||||
for line in lines:
|
||||
if header_regex.match(line):
|
||||
current_section, seperator, lead = line.partition(':')
|
||||
sections[current_section] = lead.strip()
|
||||
else:
|
||||
sections[current_section] += '\n' + line
|
||||
|
||||
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
|
||||
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||
if header in sections:
|
||||
return sections[header].strip()
|
||||
if header in coerce_method_names:
|
||||
if coerce_method_names[header] in sections:
|
||||
return sections[coerce_method_names[header]].strip()
|
||||
return sections[''].strip()
|
||||
|
||||
def get_path_fields(self, path, method):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
templated path variables.
|
||||
"""
|
||||
view = self.view
|
||||
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
||||
fields = []
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
title = ''
|
||||
description = ''
|
||||
schema_cls = coreschema.String
|
||||
kwargs = {}
|
||||
if model is not None:
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception:
|
||||
model_field = None
|
||||
|
||||
if model_field is not None and model_field.verbose_name:
|
||||
title = force_text(model_field.verbose_name)
|
||||
|
||||
if model_field is not None and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field is not None and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
|
||||
if hasattr(view, 'lookup_value_regex') and view.lookup_field == variable:
|
||||
kwargs['pattern'] = view.lookup_value_regex
|
||||
elif isinstance(model_field, models.AutoField):
|
||||
schema_cls = coreschema.Integer
|
||||
|
||||
field = coreapi.Field(
|
||||
name=variable,
|
||||
location='path',
|
||||
required=True,
|
||||
schema=schema_cls(title=title, description=description, **kwargs)
|
||||
)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_serializer_fields(self, path, method):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
request body input, as determined by the serializer class.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return []
|
||||
|
||||
if not hasattr(view, 'get_serializer'):
|
||||
return []
|
||||
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if isinstance(serializer, serializers.ListSerializer):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='data',
|
||||
location='body',
|
||||
required=True,
|
||||
schema=coreschema.Array()
|
||||
)
|
||||
]
|
||||
|
||||
if not isinstance(serializer, serializers.Serializer):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for field in serializer.fields.values():
|
||||
if field.read_only or isinstance(field, serializers.HiddenField):
|
||||
continue
|
||||
|
||||
required = field.required and method != 'PATCH'
|
||||
field = coreapi.Field(
|
||||
name=field.field_name,
|
||||
location='form',
|
||||
required=required,
|
||||
schema=field_to_schema(field)
|
||||
)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_pagination_fields(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if not is_list_view(path, method, view):
|
||||
return []
|
||||
|
||||
pagination = getattr(view, 'pagination_class', None)
|
||||
if not pagination:
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
return paginator.get_schema_fields(view)
|
||||
|
||||
def _allows_filters(self, path, method):
|
||||
"""
|
||||
Determine whether to include filter Fields in schema.
|
||||
|
||||
Default implementation looks for ModelViewSet or GenericAPIView
|
||||
actions/methods that cause filtering on the default implementation.
|
||||
|
||||
Override to adjust behaviour for your view.
|
||||
|
||||
Note: Introduced in v3.7: Initially "private" (i.e. with leading underscore)
|
||||
to allow changes based on user experience.
|
||||
"""
|
||||
if getattr(self.view, 'filter_backends', None) is None:
|
||||
return False
|
||||
|
||||
if hasattr(self.view, 'action'):
|
||||
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
|
||||
|
||||
return method.lower() in ["get", "put", "patch", "delete"]
|
||||
|
||||
def get_filter_fields(self, path, method):
|
||||
if not self._allows_filters(path, method):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for filter_backend in self.view.filter_backends:
|
||||
fields += filter_backend().get_schema_fields(self.view)
|
||||
return fields
|
||||
|
||||
def get_manual_fields(self, path, method):
|
||||
return self._manual_fields
|
||||
|
||||
@staticmethod
|
||||
def update_fields(fields, update_with):
|
||||
"""
|
||||
Update list of coreapi.Field instances, overwriting on `Field.name`.
|
||||
|
||||
Utility function to handle replacing coreapi.Field fields
|
||||
from a list by name. Used to handle `manual_fields`.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `fields`: list of `coreapi.Field` instances to update
|
||||
* `update_with: list of `coreapi.Field` instances to add or replace.
|
||||
"""
|
||||
if not update_with:
|
||||
return fields
|
||||
|
||||
by_name = OrderedDict((f.name, f) for f in fields)
|
||||
for f in update_with:
|
||||
by_name[f.name] = f
|
||||
fields = list(by_name.values())
|
||||
return fields
|
||||
|
||||
def get_encoding(self, path, method):
|
||||
"""
|
||||
Return the 'encoding' parameter to use for a given endpoint.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
# Core API supports the following request encodings over HTTP...
|
||||
supported_media_types = {
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
}
|
||||
parser_classes = getattr(view, 'parser_classes', [])
|
||||
for parser_class in parser_classes:
|
||||
media_type = getattr(parser_class, 'media_type', None)
|
||||
if media_type in supported_media_types:
|
||||
return media_type
|
||||
# Raw binary uploads are supported with "application/octet-stream"
|
||||
if media_type == '*/*':
|
||||
return 'application/octet-stream'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ManualSchema(ViewInspector):
|
||||
"""
|
||||
Allows providing a list of coreapi.Fields,
|
||||
plus an optional description.
|
||||
"""
|
||||
def __init__(self, fields, description='', encoding=None):
|
||||
"""
|
||||
Parameters:
|
||||
|
||||
* `fields`: list of `coreapi.Field` instances.
|
||||
* `description`: String description for view. Optional.
|
||||
"""
|
||||
super(ManualSchema, self).__init__()
|
||||
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
|
||||
self._fields = fields
|
||||
self._description = description
|
||||
self._encoding = encoding
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
|
||||
if base_url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=parse.urljoin(base_url, path),
|
||||
action=method.lower(),
|
||||
encoding=self._encoding,
|
||||
fields=self._fields,
|
||||
description=self._description
|
||||
)
|
||||
|
||||
|
||||
def is_enabled():
|
||||
"""Is CoreAPI Mode enabled?"""
|
||||
return issubclass(api_settings.DEFAULT_SCHEMA_CLASS, AutoSchema)
|
|
@ -4,25 +4,19 @@ generators.py # Top-down schema generation
|
|||
See schemas.__init__.py for package overview.
|
||||
"""
|
||||
import re
|
||||
from collections import Counter, OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admindocs.views import simplify_regex
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.compat import (
|
||||
URLPattern, URLResolver, coreapi, coreschema, get_original_route
|
||||
)
|
||||
from rest_framework.compat import URLPattern, URLResolver, get_original_route
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.model_meta import _get_pk
|
||||
|
||||
from .utils import is_list_view
|
||||
|
||||
|
||||
def common_path(paths):
|
||||
split_paths = [path.strip('/').split('/') for path in paths]
|
||||
|
@ -51,78 +45,6 @@ def is_api_view(callback):
|
|||
return (cls is not None) and issubclass(cls, APIView)
|
||||
|
||||
|
||||
INSERT_INTO_COLLISION_FMT = """
|
||||
Schema Naming Collision.
|
||||
|
||||
coreapi.Link for URL path {value_url} cannot be inserted into schema.
|
||||
Position conflicts with coreapi.Link for URL path {target_url}.
|
||||
|
||||
Attempted to insert link with keys: {keys}.
|
||||
|
||||
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
|
||||
to customise schema structure.
|
||||
"""
|
||||
|
||||
|
||||
class LinkNode(OrderedDict):
|
||||
def __init__(self):
|
||||
self.links = []
|
||||
self.methods_counter = Counter()
|
||||
super(LinkNode, self).__init__()
|
||||
|
||||
def get_available_key(self, preferred_key):
|
||||
if preferred_key not in self:
|
||||
return preferred_key
|
||||
|
||||
while True:
|
||||
current_val = self.methods_counter[preferred_key]
|
||||
self.methods_counter[preferred_key] += 1
|
||||
|
||||
key = '{}_{}'.format(preferred_key, current_val)
|
||||
if key not in self:
|
||||
return key
|
||||
|
||||
|
||||
def insert_into(target, keys, value):
|
||||
"""
|
||||
Nested dictionary insertion.
|
||||
|
||||
>>> example = {}
|
||||
>>> insert_into(example, ['a', 'b', 'c'], 123)
|
||||
>>> example
|
||||
LinkNode({'a': LinkNode({'b': LinkNode({'c': LinkNode(links=[123])}}})))
|
||||
"""
|
||||
for key in keys[:-1]:
|
||||
if key not in target:
|
||||
target[key] = LinkNode()
|
||||
target = target[key]
|
||||
|
||||
try:
|
||||
target.links.append((keys[-1], value))
|
||||
except TypeError:
|
||||
msg = INSERT_INTO_COLLISION_FMT.format(
|
||||
value_url=value.url,
|
||||
target_url=target.url,
|
||||
keys=keys
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def distribute_links(obj):
|
||||
for key, value in obj.items():
|
||||
distribute_links(value)
|
||||
|
||||
for preferred_key, link in obj.links:
|
||||
key = obj.get_available_key(preferred_key)
|
||||
obj[key] = link
|
||||
|
||||
|
||||
def is_custom_action(action):
|
||||
return action not in {
|
||||
'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy'
|
||||
}
|
||||
|
||||
|
||||
def endpoint_ordering(endpoint):
|
||||
path, method, callback = endpoint
|
||||
method_priority = {
|
||||
|
@ -132,7 +54,7 @@ def endpoint_ordering(endpoint):
|
|||
'PATCH': 3,
|
||||
'DELETE': 4
|
||||
}.get(method, 5)
|
||||
return (path, method_priority)
|
||||
return (method_priority,)
|
||||
|
||||
|
||||
_PATH_PARAMETER_COMPONENT_RE = re.compile(
|
||||
|
@ -140,7 +62,7 @@ _PATH_PARAMETER_COMPONENT_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
class EndpointEnumerator(object):
|
||||
class EndpointEnumerator:
|
||||
"""
|
||||
A class to determine the available API endpoints that a project exposes.
|
||||
"""
|
||||
|
@ -151,7 +73,7 @@ class EndpointEnumerator(object):
|
|||
urlconf = settings.ROOT_URLCONF
|
||||
|
||||
# Load the given URLconf module
|
||||
if isinstance(urlconf, six.string_types):
|
||||
if isinstance(urlconf, str):
|
||||
urls = import_module(urlconf)
|
||||
else:
|
||||
urls = urlconf
|
||||
|
@ -185,19 +107,20 @@ class EndpointEnumerator(object):
|
|||
)
|
||||
api_endpoints.extend(nested_endpoints)
|
||||
|
||||
api_endpoints = sorted(api_endpoints, key=endpoint_ordering)
|
||||
|
||||
return api_endpoints
|
||||
return sorted(api_endpoints, key=endpoint_ordering)
|
||||
|
||||
def get_path_from_regex(self, path_regex):
|
||||
"""
|
||||
Given a URL conf regex, return a URI template string.
|
||||
"""
|
||||
# ???: Would it be feasible to adjust this such that we generate the
|
||||
# path, plus the kwargs, plus the type from the convertor, such that we
|
||||
# could feed that straight into the parameter schema object?
|
||||
|
||||
path = simplify_regex(path_regex)
|
||||
|
||||
# Strip Django 2.0 convertors as they are incompatible with uritemplate format
|
||||
path = re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
|
||||
return path
|
||||
return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
|
||||
|
||||
def should_include_endpoint(self, path, callback):
|
||||
"""
|
||||
|
@ -232,35 +155,18 @@ class EndpointEnumerator(object):
|
|||
return [method for method in methods if method not in ('OPTIONS', 'HEAD')]
|
||||
|
||||
|
||||
class SchemaGenerator(object):
|
||||
# Map HTTP methods onto actions.
|
||||
default_mapping = {
|
||||
'get': 'retrieve',
|
||||
'post': 'create',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
'delete': 'destroy',
|
||||
}
|
||||
class BaseSchemaGenerator(object):
|
||||
endpoint_inspector_cls = EndpointEnumerator
|
||||
|
||||
# Map the method names we use for viewset actions onto external schema names.
|
||||
# These give us names that are more suitable for the external representation.
|
||||
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
|
||||
coerce_method_names = None
|
||||
|
||||
# 'pk' isn't great as an externally exposed name for an identifier,
|
||||
# so by default we prefer to use the actual model field name for schemas.
|
||||
# Set by 'SCHEMA_COERCE_PATH_PK'.
|
||||
coerce_path_pk = None
|
||||
|
||||
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
|
||||
assert coreapi, '`coreapi` must be installed for schema support.'
|
||||
assert coreschema, '`coreschema` must be installed for schema support.'
|
||||
|
||||
if url and not url.endswith('/'):
|
||||
url += '/'
|
||||
|
||||
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||
self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK
|
||||
|
||||
self.patterns = patterns
|
||||
|
@ -270,36 +176,15 @@ class SchemaGenerator(object):
|
|||
self.url = url
|
||||
self.endpoints = None
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""
|
||||
Generate a `coreapi.Document` representing the API schema.
|
||||
"""
|
||||
def _initialise_endpoints(self):
|
||||
if self.endpoints is None:
|
||||
inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf)
|
||||
self.endpoints = inspector.get_api_endpoints()
|
||||
|
||||
links = self.get_links(None if public else request)
|
||||
if not links:
|
||||
return None
|
||||
|
||||
url = self.url
|
||||
if not url and request is not None:
|
||||
url = request.build_absolute_uri()
|
||||
|
||||
distribute_links(links)
|
||||
return coreapi.Document(
|
||||
title=self.title, description=self.description,
|
||||
url=url, content=links
|
||||
)
|
||||
|
||||
def get_links(self, request=None):
|
||||
def _get_paths_and_endpoints(self, request):
|
||||
"""
|
||||
Return a dictionary containing all the links that should be
|
||||
included in the API schema.
|
||||
Generate (path, method, view) given (path, method, callback) for paths.
|
||||
"""
|
||||
links = LinkNode()
|
||||
|
||||
# Generate (path, method, view) given (path, method, callback).
|
||||
paths = []
|
||||
view_endpoints = []
|
||||
for path, method, callback in self.endpoints:
|
||||
|
@ -308,53 +193,7 @@ class SchemaGenerator(object):
|
|||
paths.append(path)
|
||||
view_endpoints.append((path, method, view))
|
||||
|
||||
# Only generate the path prefix for paths that will be included
|
||||
if not paths:
|
||||
return None
|
||||
prefix = self.determine_path_prefix(paths)
|
||||
|
||||
for path, method, view in view_endpoints:
|
||||
if not self.has_view_permissions(path, method, view):
|
||||
continue
|
||||
link = view.schema.get_link(path, method, base_url=self.url)
|
||||
subpath = path[len(prefix):]
|
||||
keys = self.get_keys(subpath, method, view)
|
||||
insert_into(links, keys, link)
|
||||
|
||||
return links
|
||||
|
||||
# Methods used when we generate a view instance from the raw callback...
|
||||
|
||||
def determine_path_prefix(self, paths):
|
||||
"""
|
||||
Given a list of all paths, return the common prefix which should be
|
||||
discounted when generating a schema structure.
|
||||
|
||||
This will be the longest common string that does not include that last
|
||||
component of the URL, or the last component before a path parameter.
|
||||
|
||||
For example:
|
||||
|
||||
/api/v1/users/
|
||||
/api/v1/users/{pk}/
|
||||
|
||||
The path prefix is '/api/v1/'
|
||||
"""
|
||||
prefixes = []
|
||||
for path in paths:
|
||||
components = path.strip('/').split('/')
|
||||
initial_components = []
|
||||
for component in components:
|
||||
if '{' in component:
|
||||
break
|
||||
initial_components.append(component)
|
||||
prefix = '/'.join(initial_components[:-1])
|
||||
if not prefix:
|
||||
# We can just break early in the case that there's at least
|
||||
# one URL that doesn't have a path prefix.
|
||||
return '/'
|
||||
prefixes.append('/' + prefix + '/')
|
||||
return common_path(prefixes)
|
||||
return paths, view_endpoints
|
||||
|
||||
def create_view(self, callback, method, request=None):
|
||||
"""
|
||||
|
@ -379,19 +218,6 @@ class SchemaGenerator(object):
|
|||
|
||||
return view
|
||||
|
||||
def has_view_permissions(self, path, method, view):
|
||||
"""
|
||||
Return `True` if the incoming request has the correct view permissions.
|
||||
"""
|
||||
if view.request is None:
|
||||
return True
|
||||
|
||||
try:
|
||||
view.check_permissions(view.request)
|
||||
except (exceptions.APIException, Http404, PermissionDenied):
|
||||
return False
|
||||
return True
|
||||
|
||||
def coerce_path(self, path, method, view):
|
||||
"""
|
||||
Coerce {pk} path arguments into the name of the model field,
|
||||
|
@ -407,48 +233,49 @@ class SchemaGenerator(object):
|
|||
field_name = 'id'
|
||||
return path.replace('{pk}', '{%s}' % field_name)
|
||||
|
||||
# Method for generating the link layout....
|
||||
def get_schema(self, request=None, public=False):
|
||||
raise NotImplementedError(".get_schema() must be implemented in subclasses.")
|
||||
|
||||
def get_keys(self, subpath, method, view):
|
||||
def determine_path_prefix(self, paths):
|
||||
"""
|
||||
Return a list of keys that should be used to layout a link within
|
||||
the schema document.
|
||||
Given a list of all paths, return the common prefix which should be
|
||||
discounted when generating a schema structure.
|
||||
|
||||
/users/ ("users", "list"), ("users", "create")
|
||||
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
|
||||
/users/enabled/ ("users", "enabled") # custom viewset list action
|
||||
/users/{pk}/star/ ("users", "star") # custom viewset detail action
|
||||
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
|
||||
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
|
||||
This will be the longest common string that does not include that last
|
||||
component of the URL, or the last component before a path parameter.
|
||||
|
||||
For example:
|
||||
|
||||
/api/v1/users/
|
||||
/api/v1/users/{pk}/
|
||||
|
||||
The path prefix is '/api/v1'
|
||||
"""
|
||||
if hasattr(view, 'action'):
|
||||
# Viewsets have explicitly named actions.
|
||||
action = view.action
|
||||
else:
|
||||
# Views have no associated action, so we determine one from the method.
|
||||
if is_list_view(subpath, method, view):
|
||||
action = 'list'
|
||||
else:
|
||||
action = self.default_mapping[method.lower()]
|
||||
prefixes = []
|
||||
for path in paths:
|
||||
components = path.strip('/').split('/')
|
||||
initial_components = []
|
||||
for component in components:
|
||||
if '{' in component:
|
||||
break
|
||||
initial_components.append(component)
|
||||
prefix = '/'.join(initial_components[:-1])
|
||||
if not prefix:
|
||||
# We can just break early in the case that there's at least
|
||||
# one URL that doesn't have a path prefix.
|
||||
return '/'
|
||||
prefixes.append('/' + prefix + '/')
|
||||
return common_path(prefixes)
|
||||
|
||||
named_path_components = [
|
||||
component for component
|
||||
in subpath.strip('/').split('/')
|
||||
if '{' not in component
|
||||
]
|
||||
def has_view_permissions(self, path, method, view):
|
||||
"""
|
||||
Return `True` if the incoming request has the correct view permissions.
|
||||
"""
|
||||
if view.request is None:
|
||||
return True
|
||||
|
||||
if is_custom_action(action):
|
||||
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
|
||||
if len(view.action_map) > 1:
|
||||
action = self.default_mapping[method.lower()]
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
return named_path_components + [action]
|
||||
else:
|
||||
return named_path_components[:-1] + [action]
|
||||
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
|
||||
# Default action, eg "/users/", "/users/{pk}/"
|
||||
return named_path_components + [action]
|
||||
try:
|
||||
view.check_permissions(view.request)
|
||||
except (exceptions.APIException, Http404, PermissionDenied):
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -1,131 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
inspectors.py # Per-endpoint view introspection
|
||||
|
||||
See schemas.__init__.py for package overview.
|
||||
"""
|
||||
import re
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import coreapi, coreschema, uritemplate
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import formatting
|
||||
|
||||
from .utils import is_list_view
|
||||
|
||||
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
|
||||
|
||||
|
||||
def field_to_schema(field):
|
||||
title = force_text(field.label) if field.label else ''
|
||||
description = force_text(field.help_text) if field.help_text else ''
|
||||
|
||||
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
|
||||
child_schema = field_to_schema(field.child)
|
||||
return coreschema.Array(
|
||||
items=child_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.DictField):
|
||||
return coreschema.Object(
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.Serializer):
|
||||
return coreschema.Object(
|
||||
properties=OrderedDict([
|
||||
(key, field_to_schema(value))
|
||||
for key, value
|
||||
in field.fields.items()
|
||||
]),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ManyRelatedField):
|
||||
related_field_schema = field_to_schema(field.child_relation)
|
||||
|
||||
return coreschema.Array(
|
||||
items=related_field_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
schema_cls = coreschema.String
|
||||
model = getattr(field.queryset, 'model', None)
|
||||
if model is not None:
|
||||
model_field = model._meta.pk
|
||||
if isinstance(model_field, models.AutoField):
|
||||
schema_cls = coreschema.Integer
|
||||
return schema_cls(title=title, description=description)
|
||||
elif isinstance(field, serializers.RelatedField):
|
||||
return coreschema.String(title=title, description=description)
|
||||
elif isinstance(field, serializers.MultipleChoiceField):
|
||||
return coreschema.Array(
|
||||
items=coreschema.Enum(enum=list(field.choices)),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
return coreschema.Enum(
|
||||
enum=list(field.choices),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
return coreschema.Boolean(title=title, description=description)
|
||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||
return coreschema.Number(title=title, description=description)
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
return coreschema.Integer(title=title, description=description)
|
||||
elif isinstance(field, serializers.DateField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date'
|
||||
)
|
||||
elif isinstance(field, serializers.DateTimeField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date-time'
|
||||
)
|
||||
elif isinstance(field, serializers.JSONField):
|
||||
return coreschema.Object(title=title, description=description)
|
||||
|
||||
if field.style.get('base_template') == 'textarea.html':
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='textarea'
|
||||
)
|
||||
|
||||
return coreschema.String(title=title, description=description)
|
||||
|
||||
|
||||
def get_pk_description(model, model_field):
|
||||
if isinstance(model_field, models.AutoField):
|
||||
value_type = _('unique integer value')
|
||||
elif isinstance(model_field, models.UUIDField):
|
||||
value_type = _('UUID string')
|
||||
else:
|
||||
value_type = _('unique value')
|
||||
|
||||
return _('A {value_type} identifying this {name}.').format(
|
||||
value_type=value_type,
|
||||
name=model._meta.verbose_name,
|
||||
)
|
||||
|
||||
|
||||
class ViewInspector(object):
|
||||
class ViewInspector:
|
||||
"""
|
||||
Descriptor class on APIView.
|
||||
|
||||
|
@ -179,326 +62,11 @@ class ViewInspector(object):
|
|||
def view(self):
|
||||
self._view = None
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
"""
|
||||
Generate `coreapi.Link` for self.view, path and method.
|
||||
|
||||
This is the main _public_ access point.
|
||||
|
||||
Parameters:
|
||||
|
||||
* path: Route path for view from URLConf.
|
||||
* method: The HTTP request method.
|
||||
* base_url: The project "mount point" as given to SchemaGenerator
|
||||
"""
|
||||
raise NotImplementedError(".get_link() must be overridden.")
|
||||
|
||||
|
||||
class AutoSchema(ViewInspector):
|
||||
"""
|
||||
Default inspector for APIView
|
||||
|
||||
Responsible for per-view introspection and schema generation.
|
||||
"""
|
||||
def __init__(self, manual_fields=None):
|
||||
"""
|
||||
Parameters:
|
||||
|
||||
* `manual_fields`: list of `coreapi.Field` instances that
|
||||
will be added to auto-generated fields, overwriting on `Field.name`
|
||||
"""
|
||||
super(AutoSchema, self).__init__()
|
||||
if manual_fields is None:
|
||||
manual_fields = []
|
||||
self._manual_fields = manual_fields
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
fields = self.get_path_fields(path, method)
|
||||
fields += self.get_serializer_fields(path, method)
|
||||
fields += self.get_pagination_fields(path, method)
|
||||
fields += self.get_filter_fields(path, method)
|
||||
|
||||
manual_fields = self.get_manual_fields(path, method)
|
||||
fields = self.update_fields(fields, manual_fields)
|
||||
|
||||
if fields and any([field.location in ('form', 'body') for field in fields]):
|
||||
encoding = self.get_encoding(path, method)
|
||||
else:
|
||||
encoding = None
|
||||
|
||||
description = self.get_description(path, method)
|
||||
|
||||
if base_url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=urlparse.urljoin(base_url, path),
|
||||
action=method.lower(),
|
||||
encoding=encoding,
|
||||
fields=fields,
|
||||
description=description
|
||||
)
|
||||
|
||||
def get_description(self, path, method):
|
||||
"""
|
||||
Determine a link description.
|
||||
|
||||
This will be based on the method docstring if one exists,
|
||||
or else the class docstring.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
method_name = getattr(view, 'action', method.lower())
|
||||
method_docstring = getattr(view, method_name, None).__doc__
|
||||
if method_docstring:
|
||||
# An explicit docstring on the method or action.
|
||||
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
|
||||
else:
|
||||
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
|
||||
|
||||
def _get_description_section(self, view, header, description):
|
||||
lines = [line for line in description.splitlines()]
|
||||
current_section = ''
|
||||
sections = {'': ''}
|
||||
|
||||
for line in lines:
|
||||
if header_regex.match(line):
|
||||
current_section, seperator, lead = line.partition(':')
|
||||
sections[current_section] = lead.strip()
|
||||
else:
|
||||
sections[current_section] += '\n' + line
|
||||
|
||||
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
|
||||
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||
if header in sections:
|
||||
return sections[header].strip()
|
||||
if header in coerce_method_names:
|
||||
if coerce_method_names[header] in sections:
|
||||
return sections[coerce_method_names[header]].strip()
|
||||
return sections[''].strip()
|
||||
|
||||
def get_path_fields(self, path, method):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
templated path variables.
|
||||
"""
|
||||
view = self.view
|
||||
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
||||
fields = []
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
title = ''
|
||||
description = ''
|
||||
schema_cls = coreschema.String
|
||||
kwargs = {}
|
||||
if model is not None:
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception:
|
||||
model_field = None
|
||||
|
||||
if model_field is not None and model_field.verbose_name:
|
||||
title = force_text(model_field.verbose_name)
|
||||
|
||||
if model_field is not None and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field is not None and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
|
||||
if hasattr(view, 'lookup_value_regex') and view.lookup_field == variable:
|
||||
kwargs['pattern'] = view.lookup_value_regex
|
||||
elif isinstance(model_field, models.AutoField):
|
||||
schema_cls = coreschema.Integer
|
||||
|
||||
field = coreapi.Field(
|
||||
name=variable,
|
||||
location='path',
|
||||
required=True,
|
||||
schema=schema_cls(title=title, description=description, **kwargs)
|
||||
)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_serializer_fields(self, path, method):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
request body input, as determined by the serializer class.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return []
|
||||
|
||||
if not hasattr(view, 'get_serializer'):
|
||||
return []
|
||||
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if isinstance(serializer, serializers.ListSerializer):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='data',
|
||||
location='body',
|
||||
required=True,
|
||||
schema=coreschema.Array()
|
||||
)
|
||||
]
|
||||
|
||||
if not isinstance(serializer, serializers.Serializer):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for field in serializer.fields.values():
|
||||
if field.read_only or isinstance(field, serializers.HiddenField):
|
||||
continue
|
||||
|
||||
required = field.required and method != 'PATCH'
|
||||
field = coreapi.Field(
|
||||
name=field.field_name,
|
||||
location='form',
|
||||
required=required,
|
||||
schema=field_to_schema(field)
|
||||
)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_pagination_fields(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if not is_list_view(path, method, view):
|
||||
return []
|
||||
|
||||
pagination = getattr(view, 'pagination_class', None)
|
||||
if not pagination:
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
return paginator.get_schema_fields(view)
|
||||
|
||||
def _allows_filters(self, path, method):
|
||||
"""
|
||||
Determine whether to include filter Fields in schema.
|
||||
|
||||
Default implementation looks for ModelViewSet or GenericAPIView
|
||||
actions/methods that cause filtering on the default implementation.
|
||||
|
||||
Override to adjust behaviour for your view.
|
||||
|
||||
Note: Introduced in v3.7: Initially "private" (i.e. with leading underscore)
|
||||
to allow changes based on user experience.
|
||||
"""
|
||||
if getattr(self.view, 'filter_backends', None) is None:
|
||||
return False
|
||||
|
||||
if hasattr(self.view, 'action'):
|
||||
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
|
||||
|
||||
return method.lower() in ["get", "put", "patch", "delete"]
|
||||
|
||||
def get_filter_fields(self, path, method):
|
||||
if not self._allows_filters(path, method):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for filter_backend in self.view.filter_backends:
|
||||
fields += filter_backend().get_schema_fields(self.view)
|
||||
return fields
|
||||
|
||||
def get_manual_fields(self, path, method):
|
||||
return self._manual_fields
|
||||
|
||||
@staticmethod
|
||||
def update_fields(fields, update_with):
|
||||
"""
|
||||
Update list of coreapi.Field instances, overwriting on `Field.name`.
|
||||
|
||||
Utility function to handle replacing coreapi.Field fields
|
||||
from a list by name. Used to handle `manual_fields`.
|
||||
|
||||
Parameters:
|
||||
|
||||
* `fields`: list of `coreapi.Field` instances to update
|
||||
* `update_with: list of `coreapi.Field` instances to add or replace.
|
||||
"""
|
||||
if not update_with:
|
||||
return fields
|
||||
|
||||
by_name = OrderedDict((f.name, f) for f in fields)
|
||||
for f in update_with:
|
||||
by_name[f.name] = f
|
||||
fields = list(by_name.values())
|
||||
return fields
|
||||
|
||||
def get_encoding(self, path, method):
|
||||
"""
|
||||
Return the 'encoding' parameter to use for a given endpoint.
|
||||
"""
|
||||
view = self.view
|
||||
|
||||
# Core API supports the following request encodings over HTTP...
|
||||
supported_media_types = {
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
}
|
||||
parser_classes = getattr(view, 'parser_classes', [])
|
||||
for parser_class in parser_classes:
|
||||
media_type = getattr(parser_class, 'media_type', None)
|
||||
if media_type in supported_media_types:
|
||||
return media_type
|
||||
# Raw binary uploads are supported with "application/octet-stream"
|
||||
if media_type == '*/*':
|
||||
return 'application/octet-stream'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ManualSchema(ViewInspector):
|
||||
"""
|
||||
Allows providing a list of coreapi.Fields,
|
||||
plus an optional description.
|
||||
"""
|
||||
def __init__(self, fields, description='', encoding=None):
|
||||
"""
|
||||
Parameters:
|
||||
|
||||
* `fields`: list of `coreapi.Field` instances.
|
||||
* `description`: String description for view. Optional.
|
||||
"""
|
||||
super(ManualSchema, self).__init__()
|
||||
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
|
||||
self._fields = fields
|
||||
self._description = description
|
||||
self._encoding = encoding
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
|
||||
if base_url and path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
return coreapi.Link(
|
||||
url=urlparse.urljoin(base_url, path),
|
||||
action=method.lower(),
|
||||
encoding=self._encoding,
|
||||
fields=self._fields,
|
||||
description=self._description
|
||||
)
|
||||
|
||||
|
||||
class DefaultSchema(ViewInspector):
|
||||
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
|
||||
def __get__(self, instance, owner):
|
||||
result = super(DefaultSchema, self).__get__(instance, owner)
|
||||
result = super().__get__(instance, owner)
|
||||
if not isinstance(result, DefaultSchema):
|
||||
return result
|
||||
|
||||
|
|
480
rest_framework/schemas/openapi.py
Normal file
|
@ -0,0 +1,480 @@
|
|||
import warnings
|
||||
|
||||
from django.core.validators import (
|
||||
DecimalValidator, EmailValidator, MaxLengthValidator, MaxValueValidator,
|
||||
MinLengthValidator, MinValueValidator, RegexValidator, URLValidator
|
||||
)
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import uritemplate
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from .generators import BaseSchemaGenerator
|
||||
from .inspectors import ViewInspector
|
||||
from .utils import get_pk_description, is_list_view
|
||||
|
||||
# Generator
|
||||
|
||||
|
||||
class SchemaGenerator(BaseSchemaGenerator):
|
||||
|
||||
def get_info(self):
|
||||
info = {
|
||||
'title': self.title,
|
||||
'version': 'TODO',
|
||||
}
|
||||
|
||||
if self.description is not None:
|
||||
info['description'] = self.description
|
||||
|
||||
return info
|
||||
|
||||
def get_paths(self, request=None):
|
||||
result = {}
|
||||
|
||||
paths, view_endpoints = self._get_paths_and_endpoints(request)
|
||||
|
||||
# Only generate the path prefix for paths that will be included
|
||||
if not paths:
|
||||
return None
|
||||
prefix = self.determine_path_prefix(paths)
|
||||
if prefix == '/': # no prefix
|
||||
prefix = ''
|
||||
|
||||
for path, method, view in view_endpoints:
|
||||
if not self.has_view_permissions(path, method, view):
|
||||
continue
|
||||
operation = view.schema.get_operation(path, method)
|
||||
subpath = path[len(prefix):]
|
||||
result.setdefault(subpath, {})
|
||||
result[subpath][method.lower()] = operation
|
||||
|
||||
return result
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""
|
||||
Generate a OpenAPI schema.
|
||||
"""
|
||||
self._initialise_endpoints()
|
||||
|
||||
paths = self.get_paths(None if public else request)
|
||||
if not paths:
|
||||
return None
|
||||
|
||||
schema = {
|
||||
'openapi': '3.0.2',
|
||||
'info': self.get_info(),
|
||||
'paths': paths,
|
||||
}
|
||||
|
||||
return schema
|
||||
|
||||
# View Inspectors
|
||||
|
||||
|
||||
class AutoSchema(ViewInspector):
|
||||
|
||||
content_types = ['application/json']
|
||||
method_mapping = {
|
||||
'get': 'Retrieve',
|
||||
'post': 'Create',
|
||||
'put': 'Update',
|
||||
'patch': 'PartialUpdate',
|
||||
'delete': 'Destroy',
|
||||
}
|
||||
|
||||
def get_operation(self, path, method):
|
||||
operation = {}
|
||||
|
||||
operation['operationId'] = self._get_operation_id(path, method)
|
||||
|
||||
parameters = []
|
||||
parameters += self._get_path_parameters(path, method)
|
||||
parameters += self._get_pagination_parameters(path, method)
|
||||
parameters += self._get_filter_parameters(path, method)
|
||||
operation['parameters'] = parameters
|
||||
|
||||
request_body = self._get_request_body(path, method)
|
||||
if request_body:
|
||||
operation['requestBody'] = request_body
|
||||
operation['responses'] = self._get_responses(path, method)
|
||||
|
||||
return operation
|
||||
|
||||
def _get_operation_id(self, path, method):
|
||||
"""
|
||||
Compute an operation ID from the model, serializer or view name.
|
||||
"""
|
||||
method_name = getattr(self.view, 'action', method.lower())
|
||||
if is_list_view(path, method, self.view):
|
||||
action = 'List'
|
||||
elif method_name not in self.method_mapping:
|
||||
action = method_name
|
||||
else:
|
||||
action = self.method_mapping[method.lower()]
|
||||
|
||||
# Try to deduce the ID from the view's model
|
||||
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
|
||||
if model is not None:
|
||||
name = model.__name__
|
||||
|
||||
# Try with the serializer class name
|
||||
elif hasattr(self.view, 'get_serializer_class'):
|
||||
name = self.view.get_serializer_class().__name__
|
||||
if name.endswith('Serializer'):
|
||||
name = name[:-10]
|
||||
|
||||
# Fallback to the view name
|
||||
else:
|
||||
name = self.view.__class__.__name__
|
||||
if name.endswith('APIView'):
|
||||
name = name[:-7]
|
||||
elif name.endswith('View'):
|
||||
name = name[:-4]
|
||||
if name.endswith(action): # ListView, UpdateAPIView, ThingDelete ...
|
||||
name = name[:-len(action)]
|
||||
|
||||
if action == 'List' and not name.endswith('s'): # ListThings instead of ListThing
|
||||
name += 's'
|
||||
|
||||
return action + name
|
||||
|
||||
def _get_path_parameters(self, path, method):
|
||||
"""
|
||||
Return a list of parameters from templated path variables.
|
||||
"""
|
||||
assert uritemplate, '`uritemplate` must be installed for OpenAPI schema support.'
|
||||
|
||||
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
|
||||
parameters = []
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
description = ''
|
||||
if model is not None: # TODO: test this.
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception:
|
||||
model_field = None
|
||||
|
||||
if model_field is not None and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field is not None and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
|
||||
parameter = {
|
||||
"name": variable,
|
||||
"in": "path",
|
||||
"required": True,
|
||||
"description": description,
|
||||
'schema': {
|
||||
'type': 'string', # TODO: integer, pattern, ...
|
||||
},
|
||||
}
|
||||
parameters.append(parameter)
|
||||
|
||||
return parameters
|
||||
|
||||
def _get_filter_parameters(self, path, method):
|
||||
if not self._allows_filters(path, method):
|
||||
return []
|
||||
parameters = []
|
||||
for filter_backend in self.view.filter_backends:
|
||||
parameters += filter_backend().get_schema_operation_parameters(self.view)
|
||||
return parameters
|
||||
|
||||
def _allows_filters(self, path, method):
|
||||
"""
|
||||
Determine whether to include filter Fields in schema.
|
||||
|
||||
Default implementation looks for ModelViewSet or GenericAPIView
|
||||
actions/methods that cause filtering on the default implementation.
|
||||
"""
|
||||
if getattr(self.view, 'filter_backends', None) is None:
|
||||
return False
|
||||
if hasattr(self.view, 'action'):
|
||||
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
|
||||
return method.lower() in ["get", "put", "patch", "delete"]
|
||||
|
||||
def _get_pagination_parameters(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if not is_list_view(path, method, view):
|
||||
return []
|
||||
|
||||
pagination = getattr(view, 'pagination_class', None)
|
||||
if not pagination:
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
return paginator.get_schema_operation_parameters(view)
|
||||
|
||||
def _map_field(self, field):
|
||||
|
||||
# Nested Serializers, `many` or not.
|
||||
if isinstance(field, serializers.ListSerializer):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': self._map_serializer(field.child)
|
||||
}
|
||||
if isinstance(field, serializers.Serializer):
|
||||
data = self._map_serializer(field)
|
||||
data['type'] = 'object'
|
||||
return data
|
||||
|
||||
# Related fields.
|
||||
if isinstance(field, serializers.ManyRelatedField):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': self._map_field(field.child_relation)
|
||||
}
|
||||
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
model = getattr(field.queryset, 'model', None)
|
||||
if model is not None:
|
||||
model_field = model._meta.pk
|
||||
if isinstance(model_field, models.AutoField):
|
||||
return {'type': 'integer'}
|
||||
|
||||
# ChoiceFields (single and multiple).
|
||||
# Q:
|
||||
# - Is 'type' required?
|
||||
# - can we determine the TYPE of a choicefield?
|
||||
if isinstance(field, serializers.MultipleChoiceField):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'enum': list(field.choices)
|
||||
},
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.ChoiceField):
|
||||
return {
|
||||
'enum': list(field.choices),
|
||||
}
|
||||
|
||||
# ListField.
|
||||
if isinstance(field, serializers.ListField):
|
||||
return {
|
||||
'type': 'array',
|
||||
}
|
||||
|
||||
# DateField and DateTimeField type is string
|
||||
if isinstance(field, serializers.DateField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'date',
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.DateTimeField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
}
|
||||
|
||||
# "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
|
||||
# see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
|
||||
# see also: https://swagger.io/docs/specification/data-models/data-types/#string
|
||||
if isinstance(field, serializers.EmailField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'email'
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.URLField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'uri'
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.UUIDField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'uuid'
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.IPAddressField):
|
||||
content = {
|
||||
'type': 'string',
|
||||
}
|
||||
if field.protocol != 'both':
|
||||
content['format'] = field.protocol
|
||||
return content
|
||||
|
||||
# DecimalField has multipleOf based on decimal_places
|
||||
if isinstance(field, serializers.DecimalField):
|
||||
content = {
|
||||
'type': 'number'
|
||||
}
|
||||
if field.decimal_places:
|
||||
content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1')
|
||||
if field.max_whole_digits:
|
||||
content['maximum'] = int(field.max_whole_digits * '9') + 1
|
||||
content['minimum'] = -content['maximum']
|
||||
self._map_min_max(field, content)
|
||||
return content
|
||||
|
||||
if isinstance(field, serializers.FloatField):
|
||||
content = {
|
||||
'type': 'number'
|
||||
}
|
||||
self._map_min_max(field, content)
|
||||
return content
|
||||
|
||||
if isinstance(field, serializers.IntegerField):
|
||||
content = {
|
||||
'type': 'integer'
|
||||
}
|
||||
self._map_min_max(field, content)
|
||||
return content
|
||||
|
||||
# Simplest cases, default to 'string' type:
|
||||
FIELD_CLASS_SCHEMA_TYPE = {
|
||||
serializers.BooleanField: 'boolean',
|
||||
serializers.JSONField: 'object',
|
||||
serializers.DictField: 'object',
|
||||
}
|
||||
return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')}
|
||||
|
||||
def _map_min_max(self, field, content):
|
||||
if field.max_value:
|
||||
content['maximum'] = field.max_value
|
||||
if field.min_value:
|
||||
content['minimum'] = field.min_value
|
||||
|
||||
def _map_serializer(self, serializer):
|
||||
# Assuming we have a valid serializer instance.
|
||||
# TODO:
|
||||
# - field is Nested or List serializer.
|
||||
# - Handle read_only/write_only for request/response differences.
|
||||
# - could do this with readOnly/writeOnly and then filter dict.
|
||||
required = []
|
||||
properties = {}
|
||||
|
||||
for field in serializer.fields.values():
|
||||
if isinstance(field, serializers.HiddenField):
|
||||
continue
|
||||
|
||||
if field.required:
|
||||
required.append(field.field_name)
|
||||
|
||||
schema = self._map_field(field)
|
||||
if field.read_only:
|
||||
schema['readOnly'] = True
|
||||
if field.write_only:
|
||||
schema['writeOnly'] = True
|
||||
if field.allow_null:
|
||||
schema['nullable'] = True
|
||||
if field.default and field.default != empty: # why don't they use None?!
|
||||
schema['default'] = field.default
|
||||
if field.help_text:
|
||||
schema['description'] = field.help_text
|
||||
self._map_field_validators(field.validators, schema)
|
||||
|
||||
properties[field.field_name] = schema
|
||||
return {
|
||||
'required': required,
|
||||
'properties': properties,
|
||||
}
|
||||
|
||||
def _map_field_validators(self, validators, schema):
|
||||
"""
|
||||
map field validators
|
||||
:param list:validators: list of field validators
|
||||
:param dict:schema: schema that the validators get added to
|
||||
"""
|
||||
for v in validators:
|
||||
# "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
|
||||
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
|
||||
if isinstance(v, EmailValidator):
|
||||
schema['format'] = 'email'
|
||||
if isinstance(v, URLValidator):
|
||||
schema['format'] = 'uri'
|
||||
if isinstance(v, RegexValidator):
|
||||
schema['pattern'] = v.regex.pattern
|
||||
elif isinstance(v, MaxLengthValidator):
|
||||
schema['maxLength'] = v.limit_value
|
||||
elif isinstance(v, MinLengthValidator):
|
||||
schema['minLength'] = v.limit_value
|
||||
elif isinstance(v, MaxValueValidator):
|
||||
schema['maximum'] = v.limit_value
|
||||
elif isinstance(v, MinValueValidator):
|
||||
schema['minimum'] = v.limit_value
|
||||
elif isinstance(v, DecimalValidator):
|
||||
if v.decimal_places:
|
||||
schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1')
|
||||
if v.max_digits:
|
||||
digits = v.max_digits
|
||||
if v.decimal_places is not None and v.decimal_places > 0:
|
||||
digits -= v.decimal_places
|
||||
schema['maximum'] = int(digits * '9') + 1
|
||||
schema['minimum'] = -schema['maximum']
|
||||
|
||||
def _get_request_body(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return {}
|
||||
|
||||
if not hasattr(view, 'get_serializer'):
|
||||
return {}
|
||||
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if not isinstance(serializer, serializers.Serializer):
|
||||
return {}
|
||||
|
||||
content = self._map_serializer(serializer)
|
||||
# No required fields for PATCH
|
||||
if method == 'PATCH':
|
||||
del content['required']
|
||||
# No read_only fields for request.
|
||||
for name, schema in content['properties'].copy().items():
|
||||
if 'readOnly' in schema:
|
||||
del content['properties'][name]
|
||||
|
||||
return {
|
||||
'content': {
|
||||
ct: {'schema': content}
|
||||
for ct in self.content_types
|
||||
}
|
||||
}
|
||||
|
||||
def _get_responses(self, path, method):
|
||||
# TODO: Handle multiple codes.
|
||||
content = {}
|
||||
view = self.view
|
||||
if hasattr(view, 'get_serializer'):
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if isinstance(serializer, serializers.Serializer):
|
||||
content = self._map_serializer(serializer)
|
||||
# No write_only fields for response.
|
||||
for name, schema in content['properties'].copy().items():
|
||||
if 'writeOnly' in schema:
|
||||
del content['properties'][name]
|
||||
content['required'] = [f for f in content['required'] if f != name]
|
||||
|
||||
return {
|
||||
'200': {
|
||||
'content': {
|
||||
ct: {'schema': content}
|
||||
for ct in self.content_types
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ utils.py # Shared helper functions
|
|||
|
||||
See schemas.__init__.py for package overview.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.mixins import RetrieveModelMixin
|
||||
|
||||
|
||||
|
@ -22,3 +25,17 @@ def is_list_view(path, method, view):
|
|||
if path_components and '{' in path_components[-1]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_pk_description(model, model_field):
|
||||
if isinstance(model_field, models.AutoField):
|
||||
value_type = _('unique integer value')
|
||||
elif isinstance(model_field, models.UUIDField):
|
||||
value_type = _('UUID string')
|
||||
else:
|
||||
value_type = _('unique value')
|
||||
|
||||
return _('A {value_type} identifying this {name}.').format(
|
||||
value_type=value_type,
|
||||
name=model._meta.verbose_name,
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ See schemas.__init__.py for package overview.
|
|||
"""
|
||||
from rest_framework import exceptions, renderers
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.schemas import coreapi
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -17,12 +18,18 @@ class SchemaView(APIView):
|
|||
public = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SchemaView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.renderer_classes is None:
|
||||
self.renderer_classes = [
|
||||
renderers.OpenAPIRenderer,
|
||||
renderers.CoreJSONRenderer
|
||||
]
|
||||
if coreapi.is_enabled():
|
||||
self.renderer_classes = [
|
||||
renderers.CoreAPIOpenAPIRenderer,
|
||||
renderers.CoreJSONRenderer
|
||||
]
|
||||
else:
|
||||
self.renderer_classes = [
|
||||
renderers.OpenAPIRenderer,
|
||||
renderers.JSONOpenAPIRenderer,
|
||||
]
|
||||
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
||||
self.renderer_classes += [renderers.BrowsableAPIRenderer]
|
||||
|
||||
|
@ -38,4 +45,4 @@ class SchemaView(APIView):
|
|||
self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
neg = self.perform_content_negotiation(self.request, force=True)
|
||||
self.request.accepted_renderer, self.request.accepted_media_type = neg
|
||||
return super(SchemaView, self).handle_exception(exc)
|
||||
return super().handle_exception(exc)
|
||||
|
|
|
@ -10,12 +10,11 @@ python primitives.
|
|||
2. The process of marshalling between python primitives and request and
|
||||
response content is handled by parsers and renderers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
|
@ -23,11 +22,11 @@ from django.db import models
|
|||
from django.db.models import DurationField as ModelDurationField
|
||||
from django.db.models.fields import Field as DjangoModelField
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.utils import six, timezone
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.compat import Mapping, postgres_fields, unicode_to_repr
|
||||
from rest_framework.compat import postgres_fields
|
||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||
from rest_framework.fields import get_error_detail, set_value
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -115,14 +114,14 @@ class BaseSerializer(Field):
|
|||
self.partial = kwargs.pop('partial', False)
|
||||
self._context = kwargs.pop('context', {})
|
||||
kwargs.pop('many', None)
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automagically create
|
||||
# `ListSerializer` classes instead when `many=True` is set.
|
||||
if kwargs.pop('many', False):
|
||||
return cls.many_init(*args, **kwargs)
|
||||
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
|
@ -315,7 +314,7 @@ class SerializerMetaclass(type):
|
|||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
|
||||
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
return super().__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
def as_serializer_error(exc):
|
||||
|
@ -344,13 +343,12 @@ def as_serializer_error(exc):
|
|||
}
|
||||
|
||||
|
||||
@six.add_metaclass(SerializerMetaclass)
|
||||
class Serializer(BaseSerializer):
|
||||
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
|
||||
default_error_messages = {
|
||||
'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
|
||||
}
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def fields(self):
|
||||
"""
|
||||
A dictionary of {field_name: field_instance}.
|
||||
|
@ -358,24 +356,22 @@ class Serializer(BaseSerializer):
|
|||
# `fields` is evaluated lazily. We do this to ensure that we don't
|
||||
# have issues importing modules that use ModelSerializers as fields,
|
||||
# even if Django's app-loading stage has not yet run.
|
||||
if not hasattr(self, '_fields'):
|
||||
self._fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
self._fields[key] = value
|
||||
return self._fields
|
||||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def _writable_fields(self):
|
||||
return [
|
||||
field for field in self.fields.values() if not field.read_only
|
||||
]
|
||||
for field in self.fields.values():
|
||||
if not field.read_only:
|
||||
yield field
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def _readable_fields(self):
|
||||
return [
|
||||
field for field in self.fields.values()
|
||||
if not field.write_only
|
||||
]
|
||||
for field in self.fields.values():
|
||||
if not field.write_only:
|
||||
yield field
|
||||
|
||||
def get_fields(self):
|
||||
"""
|
||||
|
@ -466,7 +462,7 @@ class Serializer(BaseSerializer):
|
|||
to_validate.update(value)
|
||||
else:
|
||||
to_validate = value
|
||||
super(Serializer, self).run_validators(to_validate)
|
||||
super().run_validators(to_validate)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
|
@ -535,7 +531,7 @@ class Serializer(BaseSerializer):
|
|||
return attrs
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr(representation.serializer_repr(self, indent=1))
|
||||
return representation.serializer_repr(self, indent=1)
|
||||
|
||||
# The following are used for accessing `BoundField` instances on the
|
||||
# serializer, for the purposes of presenting a form-like API onto the
|
||||
|
@ -560,12 +556,12 @@ class Serializer(BaseSerializer):
|
|||
|
||||
@property
|
||||
def data(self):
|
||||
ret = super(Serializer, self).data
|
||||
ret = super().data
|
||||
return ReturnDict(ret, serializer=self)
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
ret = super(Serializer, self).errors
|
||||
ret = super().errors
|
||||
if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
|
||||
# Edge case. Provide a more descriptive error than
|
||||
# "this field may not be null", when no data is passed.
|
||||
|
@ -591,13 +587,9 @@ class ListSerializer(BaseSerializer):
|
|||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
assert self.child is not None, '`child` is a required argument.'
|
||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||
super(ListSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super(ListSerializer, self).bind(field_name, parent)
|
||||
self.partial = self.parent.partial
|
||||
|
||||
def get_initial(self):
|
||||
if hasattr(self, 'initial_data'):
|
||||
return self.to_representation(self.initial_data)
|
||||
|
@ -649,9 +641,6 @@ class ListSerializer(BaseSerializer):
|
|||
}, code='not_a_list')
|
||||
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
if self.parent and self.partial:
|
||||
raise SkipField()
|
||||
|
||||
message = self.error_messages['empty']
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
|
@ -758,19 +747,19 @@ class ListSerializer(BaseSerializer):
|
|||
return not bool(self._errors)
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr(representation.list_repr(self, indent=1))
|
||||
return representation.list_repr(self, indent=1)
|
||||
|
||||
# Include a backlink to the serializer class on return objects.
|
||||
# Allows renderers such as HTMLFormRenderer to get the full field info.
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
ret = super(ListSerializer, self).data
|
||||
ret = super().data
|
||||
return ReturnList(ret, serializer=self)
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
ret = super(ListSerializer, self).errors
|
||||
ret = super().errors
|
||||
if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
|
||||
# Edge case. Provide a more descriptive error than
|
||||
# "this field may not be null", when no data is passed.
|
||||
|
@ -977,14 +966,22 @@ class ModelSerializer(Serializer):
|
|||
# Note that unlike `.create()` we don't need to treat many-to-many
|
||||
# relationships as being a special case. During updates we already
|
||||
# have an instance pk for the relationships to be associated with.
|
||||
m2m_fields = []
|
||||
for attr, value in validated_data.items():
|
||||
if attr in info.relations and info.relations[attr].to_many:
|
||||
field = getattr(instance, attr)
|
||||
field.set(value)
|
||||
m2m_fields.append((attr, value))
|
||||
else:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save()
|
||||
|
||||
# Note that many-to-many fields are set after updating instance.
|
||||
# Setting m2m fields triggers signals which could potentialy change
|
||||
# updated instance and we do not want it to collide with .update()
|
||||
for attr, value in m2m_fields:
|
||||
field = getattr(instance, attr)
|
||||
field.set(value)
|
||||
|
||||
return instance
|
||||
|
||||
# Determine the fields to apply...
|
||||
|
@ -1236,6 +1233,11 @@ class ModelSerializer(Serializer):
|
|||
# `allow_blank` is only valid for textual fields.
|
||||
field_kwargs.pop('allow_blank', None)
|
||||
|
||||
if postgres_fields and isinstance(model_field, postgres_fields.JSONField):
|
||||
# Populate the `encoder` argument of `JSONField` instances generated
|
||||
# for the PostgreSQL specific `JSONField`.
|
||||
field_kwargs['encoder'] = getattr(model_field, 'encoder', None)
|
||||
|
||||
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
|
||||
# Populate the `child` argument on `ListField` instances generated
|
||||
# for the PostgreSQL specific `ArrayField`.
|
||||
|
|
|
@ -18,13 +18,9 @@ This module provides the `api_setting` object, that is used to access
|
|||
REST framework settings, checking for user settings first, then falling
|
||||
back to the defaults.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.signals import setting_changed
|
||||
from django.utils import six
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from rest_framework import ISO_8601
|
||||
|
||||
|
@ -56,7 +52,7 @@ DEFAULTS = {
|
|||
'DEFAULT_FILTER_BACKENDS': (),
|
||||
|
||||
# Schema
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',
|
||||
|
||||
# Throttling
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
|
@ -166,7 +162,7 @@ def perform_import(val, setting_name):
|
|||
"""
|
||||
if val is None:
|
||||
return None
|
||||
elif isinstance(val, six.string_types):
|
||||
elif isinstance(val, str):
|
||||
return import_from_string(val, setting_name)
|
||||
elif isinstance(val, (list, tuple)):
|
||||
return [import_from_string(item, setting_name) for item in val]
|
||||
|
@ -178,16 +174,13 @@ def import_from_string(val, setting_name):
|
|||
Attempt to import a class from a string representation.
|
||||
"""
|
||||
try:
|
||||
# Nod to tastypie's use of importlib.
|
||||
module_path, class_name = val.rsplit('.', 1)
|
||||
module = import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
return import_string(val)
|
||||
except ImportError as e:
|
||||
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
||||
class APISettings(object):
|
||||
class APISettings:
|
||||
"""
|
||||
A settings object, that allows API settings to be accessed as properties.
|
||||
For example:
|
||||
|
|
2
rest_framework/static/rest_framework/js/jquery-3.4.1.min.js
vendored
Normal file
|
@ -5,7 +5,6 @@ See RFC 2616 - https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
|||
And RFC 6585 - https://tools.ietf.org/html/rfc6585
|
||||
And RFC 4918 - https://tools.ietf.org/html/rfc4918
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
def is_informational(code):
|
||||
|
@ -38,6 +37,8 @@ HTTP_204_NO_CONTENT = 204
|
|||
HTTP_205_RESET_CONTENT = 205
|
||||
HTTP_206_PARTIAL_CONTENT = 206
|
||||
HTTP_207_MULTI_STATUS = 207
|
||||
HTTP_208_ALREADY_REPORTED = 208
|
||||
HTTP_226_IM_USED = 226
|
||||
HTTP_300_MULTIPLE_CHOICES = 300
|
||||
HTTP_301_MOVED_PERMANENTLY = 301
|
||||
HTTP_302_FOUND = 302
|
||||
|
@ -46,6 +47,7 @@ HTTP_304_NOT_MODIFIED = 304
|
|||
HTTP_305_USE_PROXY = 305
|
||||
HTTP_306_RESERVED = 306
|
||||
HTTP_307_TEMPORARY_REDIRECT = 307
|
||||
HTTP_308_PERMANENT_REDIRECT = 308
|
||||
HTTP_400_BAD_REQUEST = 400
|
||||
HTTP_401_UNAUTHORIZED = 401
|
||||
HTTP_402_PAYMENT_REQUIRED = 402
|
||||
|
@ -67,6 +69,7 @@ HTTP_417_EXPECTATION_FAILED = 417
|
|||
HTTP_422_UNPROCESSABLE_ENTITY = 422
|
||||
HTTP_423_LOCKED = 423
|
||||
HTTP_424_FAILED_DEPENDENCY = 424
|
||||
HTTP_426_UPGRADE_REQUIRED = 426
|
||||
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||
|
@ -77,5 +80,9 @@ HTTP_502_BAD_GATEWAY = 502
|
|||
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
||||
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
|
||||
HTTP_507_INSUFFICIENT_STORAGE = 507
|
||||
HTTP_508_LOOP_DETECTED = 508
|
||||
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509
|
||||
HTTP_510_NOT_EXTENDED = 510
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||
|
|