diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d7c23d635 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://fund.django-rest-framework.org/topics/funding/ diff --git a/.travis.yml b/.travis.yml index 9543cb452..a4a4ed8b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 120000 index 000000000..025215c5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +docs/community/release-notes.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c947a7b8c..2f1aad08f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.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. diff --git a/README.md b/README.md index 66079edf0..6b1a84586 100644 --- a/README.md +++ b/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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..d3faefa3c --- /dev/null +++ b/SECURITY.md @@ -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 diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 5b8a9844f..3879bd70a 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -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 diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 5342345e4..502a0a9a9 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -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 diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index 8112a2e80..3a4b0357f 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -1,4 +1,7 @@ -source: negotiation.py +--- +source: + - negotiation.py +--- # Content negotiation diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 820e6d3b8..d7d73a2f2 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -1,4 +1,7 @@ -source: exceptions.py +--- +source: + - exceptions.py +--- # Exceptions diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ede4f15ad..ecda0eed5 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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=, min_length=None, max_length=None)` +**Signature**: `ListField(child=, 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=)` +**Signature**: `DictField(child=, 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=)` +**Signature**: `HStoreField(child=, 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): diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 8a500f386..be6e54afd 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -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 diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md index 629f003f3..39c812c3a 100644 --- a/docs/api-guide/format-suffixes.md +++ b/docs/api-guide/format-suffixes.md @@ -1,4 +1,7 @@ -source: urlpatterns.py +--- +source: + - urlpatterns.py +--- # Format suffixes diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index a0ed7bdea..064ef9435 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -1,5 +1,8 @@ -source: mixins.py - generics.py +--- +source: + - mixins.py + - generics.py +--- # Generic views diff --git a/docs/api-guide/metadata.md b/docs/api-guide/metadata.md index a3ba9ac20..fdb778626 100644 --- a/docs/api-guide/metadata.md +++ b/docs/api-guide/metadata.md @@ -1,4 +1,7 @@ -source: metadata.py +--- +source: + - metadata.py +--- # Metadata diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index be48ae7e5..f5674a191 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -1,4 +1,7 @@ -source: parsers.py +--- +source: + - parsers.py +--- # Parsers diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 901f810c5..2c84e1a28 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -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 diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8665e80f6..aa7f23aff 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -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 diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 4ec409681..4e54874f0 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -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 diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 28450f082..967303dd1 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -1,4 +1,7 @@ -source: request.py +--- +source: + - request.py +--- # Requests diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index e9c2d41f1..1a56b0101 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -1,4 +1,7 @@ -source: response.py +--- +source: + - response.py +--- # Responses diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 00abcf571..70df42b8f 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -1,4 +1,7 @@ -source: reverse.py +--- +source: + - reverse.py +--- # Returning URLs diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 09c6c39cb..5f6802222 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -1,4 +1,7 @@ -source: routers.py +--- +source: + - routers.py +--- # Routers diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index b09b1606e..94332c0ed 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -1,4 +1,7 @@ -source: schemas.py +--- +source: + - schemas.py +--- # Schemas diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index e25053936..0bc90b40a 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -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__'): diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 85e38185e..1da79a477 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -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. diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index 1016f3374..a37ba15d4 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -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 diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 5ca01b4e7..369e2c07b 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -1,4 +1,7 @@ -source: test.py +--- +source: + - test.py +--- # Testing diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index dade47460..713cc47cc 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -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 diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 3b50442cc..3c3b3d15c 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -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 `UniqueForValidation` 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 `UniqueForValidator` 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. --- diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index c106e536d..ad76ced3d 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -1,4 +1,7 @@ -source: versioning.py +--- +source: + - versioning.py +--- # Versioning diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 7b2c4eff7..29e7997aa 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -1,5 +1,8 @@ -source: decorators.py - views.py +--- +source: + - decorators.py + - views.py +--- # Class-based Views diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index e7cf4d48f..cd765d3e6 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -1,4 +1,7 @@ -source: viewsets.py +--- +source: + - viewsets.py +--- # ViewSets diff --git a/docs/community/3.0-announcement.md b/docs/community/3.0-announcement.md index dc118d70c..7a29b5554 100644 --- a/docs/community/3.0-announcement.md +++ b/docs/community/3.0-announcement.md @@ -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__'): diff --git a/docs/community/3.6-announcement.md b/docs/community/3.6-announcement.md index c6e8dfa06..c41ad8ecb 100644 --- a/docs/community/3.6-announcement.md +++ b/docs/community/3.6-announcement.md @@ -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. diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 9cc6ccee0..cb67100d2 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -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. diff --git a/docs/community/jobs.md b/docs/community/jobs.md index e74b78c7f..5f3d60b55 100644 --- a/docs/community/jobs.md +++ b/docs/community/jobs.md @@ -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/ diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 0f08342f5..4ed89d405 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -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 [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 + + +[gh6613]: https://github.com/encode/django-rest-framework/issues/6613 + + +[gh6680]: https://github.com/encode/django-rest-framework/issues/6680 +[gh6317]: https://github.com/encode/django-rest-framework/issues/6317 diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index ace54f6f7..cbe210866 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -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 diff --git a/docs/img/drfdocs.png b/docs/img/drfdocs.png deleted file mode 100644 index 0cccb41f7..000000000 Binary files a/docs/img/drfdocs.png and /dev/null differ diff --git a/docs/img/premium/cadre-readme.png b/docs/img/premium/cadre-readme.png index b61539469..08290b727 100644 Binary files a/docs/img/premium/cadre-readme.png and b/docs/img/premium/cadre-readme.png differ diff --git a/docs/img/premium/esg-readme.png b/docs/img/premium/esg-readme.png new file mode 100644 index 000000000..5aeb93fd2 Binary files /dev/null and b/docs/img/premium/esg-readme.png differ diff --git a/docs/img/premium/kloudless-readme.png b/docs/img/premium/kloudless-readme.png index 5d32b31b6..e2f05831d 100644 Binary files a/docs/img/premium/kloudless-readme.png and b/docs/img/premium/kloudless-readme.png differ diff --git a/docs/img/premium/lightson-readme.png b/docs/img/premium/lightson-readme.png index 3c8c6c62a..82cd61364 100644 Binary files a/docs/img/premium/lightson-readme.png and b/docs/img/premium/lightson-readme.png differ diff --git a/docs/img/premium/load-impact-readme.png b/docs/img/premium/load-impact-readme.png deleted file mode 100644 index c46d36ada..000000000 Binary files a/docs/img/premium/load-impact-readme.png and /dev/null differ diff --git a/docs/img/premium/machinalis-readme.png b/docs/img/premium/machinalis-readme.png deleted file mode 100644 index cd98c23c7..000000000 Binary files a/docs/img/premium/machinalis-readme.png and /dev/null differ diff --git a/docs/img/premium/micropyramid-readme.png b/docs/img/premium/micropyramid-readme.png deleted file mode 100644 index 9fa9500e1..000000000 Binary files a/docs/img/premium/micropyramid-readme.png and /dev/null differ diff --git a/docs/img/premium/rollbar-readme.png b/docs/img/premium/rollbar-readme.png index b0655f783..630cddb32 100644 Binary files a/docs/img/premium/rollbar-readme.png and b/docs/img/premium/rollbar-readme.png differ diff --git a/docs/img/premium/rover-readme.png b/docs/img/premium/rover-readme.png deleted file mode 100644 index b8055d62e..000000000 Binary files a/docs/img/premium/rover-readme.png and /dev/null differ diff --git a/docs/img/premium/sentry-readme.png b/docs/img/premium/sentry-readme.png index 5536ce52f..b322e3735 100644 Binary files a/docs/img/premium/sentry-readme.png and b/docs/img/premium/sentry-readme.png differ diff --git a/docs/img/premium/stream-readme.png b/docs/img/premium/stream-readme.png index fc3733c70..967ee7fc8 100644 Binary files a/docs/img/premium/stream-readme.png and b/docs/img/premium/stream-readme.png differ diff --git a/docs/index.md b/docs/index.md index 9f5d3fa15..4804f05fa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,7 +68,7 @@ continued development by **[signing up for a paid plan][funding]**.
-*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 diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 7eab08ecf..e5bf57530 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -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 diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 224ebf25b..22fe49e39 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -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/ diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index cbec2501b..8b02b888e 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -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 diff --git a/docs_theme/main.html b/docs_theme/main.html index b60b231c2..1a1175902 100644 --- a/docs_theme/main.html +++ b/docs_theme/main.html @@ -141,7 +141,7 @@ - + diff --git a/mkdocs.yml b/mkdocs.yml index 83b3c8141..b855b69b2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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' diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index 012ec99f1..73158043e 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,2 +1,2 @@ # MkDocs to build our documentation. -mkdocs==0.16.3 +mkdocs==1.0.4 diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index c800a5891..c5be70c3d 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -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 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index a2a2fa753..83ec9ab9e 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -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 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 55c06982d..cead7568d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -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 diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 25150d525..1e30728d3 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -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. """ diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py index 1a507249b..750e21cf4 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -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) diff --git a/rest_framework/authtoken/apps.py b/rest_framework/authtoken/apps.py index ad01cb404..f90fe961e 100644 --- a/rest_framework/authtoken/apps.py +++ b/rest_framework/authtoken/apps.py @@ -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): diff --git a/rest_framework/authtoken/management/commands/drf_create_token.py b/rest_framework/authtoken/management/commands/drf_create_token.py index 8e06812db..3d6539244 100644 --- a/rest_framework/authtoken/management/commands/drf_create_token.py +++ b/rest_framework/authtoken/management/commands/drf_create_token.py @@ -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)) diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 75780fedf..6a46ccfff 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py index 9f7e58e22..43119099a 100644 --- a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py +++ b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7e96eff93..bff42d3de 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -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() diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index e5f46dd66..bb552f3e5 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -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 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9422e6ad5..8d28b49e8 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -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. diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 30bfcc4e5..eb1cad9e4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -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 diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index f79b16129..a91138026 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -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): diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c8f65db0e..77767b2c5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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)) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 8c221831a..98b0f550b 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -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', + }, + }, + ] diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 8d0bf284a..c39b02ab7 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -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 diff --git a/rest_framework/management/commands/generateschema.py b/rest_framework/management/commands/generateschema.py index 591073ba0..a7763492c 100644 --- a/rest_framework/management/commands/generateschema.py +++ b/rest_framework/management/commands/generateschema.py @@ -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 diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 9f9324469..42442f91c 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -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. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index de10d6930..7fa8947cb 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -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. """ diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index ca1b59f12..76113a827 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -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 diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b11d7cdf3..4d65d080a 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -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 diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 35d0d1aa7..978576a71 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -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 diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 5d75f54ba..3a8c58064 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -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. """ diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 31c1e7561..3c2132c5b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -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') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index f043e6327..772ef3c2c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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') diff --git a/rest_framework/request.py b/rest_framework/request.py index a6d92e2bd..ec4b749c2 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -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. diff --git a/rest_framework/response.py b/rest_framework/response.py index bf0663255..495423734 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -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' diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index e9cf737f1..55bf74af1 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -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) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 1cacea181..ee5760e81 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -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) diff --git a/rest_framework/schemas/__init__.py b/rest_framework/schemas/__init__.py index ba0ec6536..8fdb2d86a 100644 --- a/rest_framework/schemas/__init__.py +++ b/rest_framework/schemas/__init__.py @@ -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, diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py new file mode 100644 index 000000000..5cf789f9f --- /dev/null +++ b/rest_framework/schemas/coreapi.py @@ -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) diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index db226a6c1..c80ace7d3 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -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}', path) - return path + return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g}', 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 diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 85142edce..86fcdc435 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -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 diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py new file mode 100644 index 000000000..eeb353329 --- /dev/null +++ b/rest_framework/schemas/openapi.py @@ -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 + } + } + } diff --git a/rest_framework/schemas/utils.py b/rest_framework/schemas/utils.py index 76437a20a..6724eb428 100644 --- a/rest_framework/schemas/utils.py +++ b/rest_framework/schemas/utils.py @@ -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, + ) diff --git a/rest_framework/schemas/views.py b/rest_framework/schemas/views.py index f5e327a94..527a23236 100644 --- a/rest_framework/schemas/views.py +++ b/rest_framework/schemas/views.py @@ -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) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9830edb3f..b153c067f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -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`. diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8db9c81ed..3520eae36 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -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: diff --git a/rest_framework/static/rest_framework/js/jquery-3.3.1.min.js b/rest_framework/static/rest_framework/js/jquery-3.3.1.min.js deleted file mode 100644 index 4d9b3a258..000000000 --- a/rest_framework/static/rest_framework/js/jquery-3.3.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" - + diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 6d740f2b5..5d9d80b05 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -293,7 +293,7 @@ csrfToken: "{% if request %}{{ csrf_token }}{% endif %}" }; - + diff --git a/rest_framework/templates/rest_framework/docs/auth/session.html b/rest_framework/templates/rest_framework/docs/auth/session.html index d09d3f2aa..59430d95e 100644 --- a/rest_framework/templates/rest_framework/docs/auth/session.html +++ b/rest_framework/templates/rest_framework/docs/auth/session.html @@ -12,7 +12,7 @@