diff --git a/.travis.yml b/.travis.yml index a4a4ed8b5..f89e77531 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,11 +21,13 @@ matrix: - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=master } - - { python: "3.7", env: TOXENV=base } - - { python: "3.7", env: TOXENV=lint } - - { python: "3.7", env: TOXENV=docs } + - { python: "3.8", env: DJANGO=master } - - python: "3.7" + - { python: "3.8", env: TOXENV=base } + - { python: "3.8", env: TOXENV=lint } + - { python: "3.8", env: TOXENV=docs } + + - python: "3.8" env: TOXENV=dist script: - python setup.py bdist_wheel diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 502a0a9a9..96517b15e 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -17,11 +17,16 @@ other cache decorators such as [`cache_page`][page] and [`vary_on_cookie`][cookie]. ```python +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie + from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import viewsets -class UserViewSet(viewsets.Viewset): + +class UserViewSet(viewsets.ViewSet): # Cache requested url for each user for 2 hours @method_decorator(cache_page(60*60*2)) @@ -32,6 +37,7 @@ class UserViewSet(viewsets.Viewset): } return Response(content) + class PostView(APIView): # Cache page for the requested url diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 92c692bf5..29cb5aec9 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -713,7 +713,7 @@ the coordinate pair: fields = ['label', 'coordinates'] Note that this example doesn't handle validation. Partly for that reason, in a -real project, the coordinate nesting might be better handled with a nested serialiser +real project, the coordinate nesting might be better handled with a nested serializer using `source='*'`, with two `IntegerField` instances, each with their own `source` pointing to the relevant field. @@ -746,7 +746,7 @@ suitable for updating our target object. With `source='*'`, the return from ('y_coordinate', 4), ('x_coordinate', 3)]) -For completeness lets do the same thing again but with the nested serialiser +For completeness lets do the same thing again but with the nested serializer approach suggested above: class NestedCoordinateSerializer(serializers.Serializer): @@ -768,14 +768,14 @@ declarations. It's our `NestedCoordinateSerializer` that takes `source='*'`. Our new `DataPointSerializer` exhibits the same behaviour as the custom field approach. -Serialising: +Serializing: >>> out_serializer = DataPointSerializer(instance) >>> out_serializer.data ReturnDict([('label', 'testing'), ('coordinates', OrderedDict([('x', 1), ('y', 2)]))]) -Deserialising: +Deserializing: >>> in_serializer = DataPointSerializer(data=data) >>> in_serializer.is_valid() @@ -802,8 +802,8 @@ But we also get the built-in validation for free: {'x': ['A valid integer is required.'], 'y': ['A valid integer is required.']})]) -For this reason, the nested serialiser approach would be the first to try. You -would use the custom field approach when the nested serialiser becomes infeasible +For this reason, the nested serializer approach would be the first to try. You +would use the custom field approach when the nested serializer becomes infeasible or overly complex. diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 8d9ead107..a2f19ff2e 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -378,10 +378,6 @@ If you need to generic PUT-as-create behavior you may want to include something The following third party packages provide additional generic view implementations. -## Django REST Framework bulk - -The [django-rest-framework-bulk package][django-rest-framework-bulk] implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests. - ## Django Rest Multiple Models [Django Rest Multiple Models][django-rest-multiple-models] provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request. @@ -394,5 +390,4 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements [RetrieveModelMixin]: #retrievemodelmixin [UpdateModelMixin]: #updatemodelmixin [DestroyModelMixin]: #destroymodelmixin -[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk [django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 14f197b21..ef6efec5e 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -245,7 +245,9 @@ This field is always read-only. # Nested relationships -Nested relationships can be expressed by using serializers as fields. +As opposed to previously discussed _references_ to another entity, the referred entity can instead also be embedded or _nested_ +in the representation of the object that refers to it. +Such nested relationships can be expressed by using serializers as fields. If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 3bc083893..1c336953c 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -49,7 +49,7 @@ If a client sends a request with a content-type that cannot be parsed then a `Un # Content negotiation -The request exposes some properties that allow you to determine the result of the content negotiation stage. This allows you to implement behaviour such as selecting a different serialisation schemes for different media types. +The request exposes some properties that allow you to determine the result of the content negotiation stage. This allows you to implement behaviour such as selecting a different serialization schemes for different media types. ## .accepted_renderer diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index 1a56b0101..dbdc8ff2c 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -94,5 +94,5 @@ As with any other `TemplateResponse`, this method is called to render the serial You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle. -[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/ +[cite]: https://docs.djangoproject.com/en/stable/ref/template-response/ [statuscodes]: status-codes.md diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index bdbb952dc..e33a2a611 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -73,7 +73,7 @@ The `get_schema_view()` helper takes the following keyword arguments: * `title`: May be used to provide a descriptive title for the schema definition. * `description`: Longer descriptive text. -* `version`: The version of the API. Defaults to `0.1.0`. +* `version`: The version of the API. * `url`: May be used to pass a canonical base URL for the schema. schema_view = get_schema_view( @@ -90,6 +90,7 @@ The `get_schema_view()` helper takes the following keyword arguments: url='https://www.example.org/api/', urlconf='myproject.urls' ) + * `patterns`: List of url patterns to limit the schema introspection to. If you only want the `myproject.api` urls to be exposed in the schema: @@ -114,7 +115,6 @@ The `get_schema_view()` helper takes the following keyword arguments: * `renderer_classes`: May be used to pass the set of renderer classes that can be used to render the API root endpoint. - ## Customizing Schema Generation You may customize schema generation at the level of the schema as a whole, or @@ -154,7 +154,7 @@ Returns a dictionary that represents the OpenAPI schema: The `request` argument is optional, and may be used if you want to apply per-user permissions to the resulting schema generation. -This is a good point to override if you want to customise the generated +This is a good point to override if you want to customize the generated dictionary, for example to add custom [specification extensions][openapi-specification-extensions]. @@ -176,21 +176,20 @@ for each view, allowed method, and path. **Note**: For basic `APIView` subclasses, default introspection is essentially limited to the URL kwarg path parameters. For `GenericAPIView` subclasses, which includes all the provided class based views, `AutoSchema` will -attempt to introspect serialiser, pagination and filter fields, as well as +attempt to introspect serializer, pagination and filter fields, as well as provide richer path field descriptions. (The key hooks here are the relevant `GenericAPIView` attributes and methods: `get_serializer`, `pagination_class`, `filter_backends` and so on.) --- -In order to customise the operation generation, you should provide an `AutoSchema` subclass, overriding `get_operation()` as you need: - +In order to customize the operation generation, you should provide an `AutoSchema` subclass, overriding `get_operation()` as you need: from rest_framework.views import APIView from rest_framework.schemas.openapi import AutoSchema class CustomSchema(AutoSchema): - def get_link(...): + def get_operation(...): # Implement custom introspection here (or in other sub-methods) class CustomView(APIView): @@ -218,4 +217,4 @@ project you may adjust `settings.DEFAULT_SCHEMA_CLASS` appropriately. [openapi]: https://github.com/OAI/OpenAPI-Specification [openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions -[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject \ No newline at end of file +[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index d6d4d5e8c..4679b1ed1 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -887,10 +887,10 @@ To implement a read-only serializer using the `BaseSerializer` class, we just ne It's simple to create a read-only serializer for converting `HighScore` instances into primitive data types. class HighScoreSerializer(serializers.BaseSerializer): - def to_representation(self, obj): + def to_representation(self, instance): return { - 'score': obj.score, - 'player_name': obj.player_name + 'score': instance.score, + 'player_name': instance.player_name } We can now use this class to serialize single `HighScore` instances: @@ -945,10 +945,10 @@ Here's a complete example of our previous `HighScoreSerializer`, that's been upd 'player_name': player_name } - def to_representation(self, obj): + def to_representation(self, instance): return { - 'score': obj.score, - 'player_name': obj.player_name + 'score': instance.score, + 'player_name': instance.player_name } def create(self, validated_data): @@ -965,10 +965,10 @@ The following class is an example of a generic serializer that can handle coerci A read-only serializer that coerces arbitrary complex objects into primitive representations. """ - def to_representation(self, obj): + def to_representation(self, instance): output = {} - for attribute_name in dir(obj): - attribute = getattr(obj, attribute_name) + for attribute_name in dir(instance): + attribute = getattr(instance, attribute_name) if attribute_name.startswith('_'): # Ignore private attributes. pass @@ -1010,7 +1010,7 @@ Some reasons this might be useful include... The signatures for these methods are as follows: -#### `.to_representation(self, obj)` +#### `.to_representation(self, instance)` Takes the object instance that requires serialization, and should return a primitive representation. Typically this means returning a structure of built-in Python datatypes. The exact types that can be handled will depend on the render classes you have configured for your API. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 768e343a7..d42000260 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -101,7 +101,7 @@ Default: `'rest_framework.negotiation.DefaultContentNegotiation'` A view inspector class that will be used for schema generation. -Default: `'rest_framework.schemas.AutoSchema'` +Default: `'rest_framework.schemas.openapi.AutoSchema'` --- diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index ad76ced3d..6076b1ed2 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -132,12 +132,12 @@ This scheme requires the client to specify the version as part of the URL path. Your URL conf must include a pattern that matches the version with a `'version'` keyword argument, so that this information is available to the versioning scheme. urlpatterns = [ - url( + re_path( r'^(?P(v1|v2))/bookings/$', bookings_list, name='bookings-list' ), - url( + re_path( r'^(?P(v1|v2))/bookings/(?P[0-9]+)/$', bookings_detail, name='bookings-detail' @@ -158,14 +158,14 @@ In the following example we're giving a set of views two different possible URL # bookings/urls.py urlpatterns = [ - url(r'^$', bookings_list, name='bookings-list'), - url(r'^(?P[0-9]+)/$', bookings_detail, name='bookings-detail') + re_path(r'^$', bookings_list, name='bookings-list'), + re_path(r'^(?P[0-9]+)/$', bookings_detail, name='bookings-detail') ] # urls.py urlpatterns = [ - url(r'^v1/bookings/', include('bookings.urls', namespace='v1')), - url(r'^v2/bookings/', include('bookings.urls', namespace='v2')) + re_path(r'^v1/bookings/', include('bookings.urls', namespace='v1')), + re_path(r'^v2/bookings/', include('bookings.urls', namespace='v2')) ] Both `URLPathVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLPathVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects. diff --git a/docs/community/3.10-announcement.md b/docs/community/3.10-announcement.md index 065dd3480..578e900dc 100644 --- a/docs/community/3.10-announcement.md +++ b/docs/community/3.10-announcement.md @@ -84,7 +84,7 @@ urlpatterns = [ ### Customization -For customizations that you want to apply across the the entire API, you can subclass `rest_framework.schemas.openapi.SchemaGenerator` and provide it as an argument +For customizations that you want to apply across the entire API, you can subclass `rest_framework.schemas.openapi.SchemaGenerator` and provide it as an argument to the `generateschema` command or `get_schema_view()` helper function. For specific per-view customizations, you can subclass `AutoSchema`, diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 5d7dab561..293c65e24 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -195,7 +195,6 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons The following issues still need to be addressed: -* [Consider moving the repo into a proper GitHub organization][github-org]. * Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin. * Document ownership of the [live example][sandbox] API. * Document ownership of the [mailing list][mailing-list] and IRC channel. @@ -206,6 +205,5 @@ The following issues still need to be addressed: [transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ [transifex-client]: https://pypi.org/project/transifex-client/ [translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations -[github-org]: https://github.com/encode/django-rest-framework/issues/2162 [sandbox]: https://restframework.herokuapp.com/ [mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index cdaa35044..283dbae67 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -222,11 +222,11 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10. def perform_create(self, serializer): serializer.save(owner=self.request.user) - Alternatively you may override `save()` or `create()` or `update()` on the serialiser as appropriate. + Alternatively you may override `save()` or `create()` or `update()` on the serializer as appropriate. * Correct allow_null behaviour when required=False [#5888][gh5888] - Without an explicit `default`, `allow_null` implies a default of `null` for outgoing serialisation. Previously such + Without an explicit `default`, `allow_null` implies a default of `null` for outgoing serialization. Previously such fields were being skipped when read-only or otherwise not required. **Possible backwards compatibility break** if you were relying on such fields being excluded from the outgoing @@ -464,7 +464,7 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10. * Deprecated `exclude_from_schema` on `APIView` and `api_view` decorator. Set `schema = None` or `@schema(None)` as appropriate. [#5422][gh5422] * Timezone-aware `DateTimeField`s now respect active or default `timezone` during serialization, instead of always using UTC. [#5435][gh5435] - Resolves inconsistency whereby instances were serialised with supplied datetime for `create` but UTC for `retrieve`. [#3732][gh3732] + Resolves inconsistency whereby instances were serialized with supplied datetime for `create` but UTC for `retrieve`. [#3732][gh3732] **Possible backwards compatibility break** if you were relying on datetime strings being UTC. Have client interpret datetimes or [set default or active timezone (docs)][djangodocs-set-timezone] to UTC if needed. diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 222534233..baa30fd0c 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -222,7 +222,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ### Views -* [djangorestframework-bulk][djangorestframework-bulk] - Implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests. * [django-rest-multiple-models][django-rest-multiple-models] - Provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request. ### Routers @@ -254,8 +253,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ### Misc * [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome. -* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer. -* [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI. +* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serializer that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer. * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. * [gaiarestframework][gaiarestframework] - Utils for django-rest-framework * [drf-extensions][drf-extensions] - A collection of custom extensions @@ -273,6 +271,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [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. * [drf-viewset-profiler][drf-viewset-profiler] - Lib to profile all methods from a viewset line by line. +* [djangorestframework-features][djangorestframework-features] - Advanced schema generation and more based on named features. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework @@ -306,7 +305,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [drf-compound-fields]: https://github.com/estebistec/drf-compound-fields [django-extra-fields]: https://github.com/Hipo/drf-extra-fields -[djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk [django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db.rest]: https://wq.io/docs/about-rest @@ -318,7 +316,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-rapidjson]: https://github.com/allisson/django-rest-framework-rapidjson [djangorestframework-chain]: https://github.com/philipn/django-rest-framework-chain [djangorestrelationalhyperlink]: https://github.com/fredkingham/django_rest_model_hyperlink_serializers_project -[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger [django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework [drf-extensions]: https://github.com/chibisov/drf-extensions @@ -356,3 +353,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt [django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian [drf-viewset-profiler]: https://github.com/fvlima/drf-viewset-profiler +[djangorestframework-features]: https://github.com/cloudcode-hungary/django-rest-framework-features/ diff --git a/docs/coreapi/schemas.md b/docs/coreapi/schemas.md index 6ee620343..69606f853 100644 --- a/docs/coreapi/schemas.md +++ b/docs/coreapi/schemas.md @@ -191,7 +191,7 @@ each view, allowed method and path.) **Note**: For basic `APIView` subclasses, default introspection is essentially limited to the URL kwarg path parameters. For `GenericAPIView` subclasses, which includes all the provided class based views, `AutoSchema` will -attempt to introspect serialiser, pagination and filter fields, as well as +attempt to introspect serializer, pagination and filter fields, as well as provide richer path field descriptions. (The key hooks here are the relevant `GenericAPIView` attributes and methods: `get_serializer`, `pagination_class`, `filter_backends` and so on.) diff --git a/docs/img/apiary.png b/docs/img/apiary.png deleted file mode 100644 index 923d384eb..000000000 Binary files a/docs/img/apiary.png and /dev/null differ diff --git a/docs/img/django-rest-swagger.png b/docs/img/django-rest-swagger.png deleted file mode 100644 index 96a6b2380..000000000 Binary files a/docs/img/django-rest-swagger.png and /dev/null differ diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index cf3977d90..5c806ea7e 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -140,56 +140,6 @@ This also translates into a very useful interactive documentation viewer in the --- -#### 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. - -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. - -This package is fully documented, well supported, and comes highly recommended. - -![Screenshot - Django REST Swagger][image-django-rest-swagger] - ---- - -### DRF AutoDocs - -Oleksander Mashianovs' [DRF Auto Docs][drfautodocs-repo] automated api renderer. - -Collects almost all the code you written into documentation effortlessly. - -Supports: - - * functional view docs - * tree-like structure - * Docstrings: - * markdown - * preserve space & newlines - * formatting with nice syntax - * Fields: - * choices rendering - * help_text (to specify SerializerMethodField output, etc) - * smart read_only/required rendering - * Endpoint properties: - * filter_backends - * authentication_classes - * permission_classes - * extra url params(GET params) - -![whole structure](http://joxi.ru/52aBGNI4k3oyA0.jpg) - ---- - -#### Apiary - -There are various other online tools and services for providing API documentation. One notable service is [Apiary][apiary]. With Apiary, you describe your API using a simple markdown-like syntax. The generated documentation includes API interaction, a mock server for testing & prototyping, and various other tools. - -![Screenshot - Apiary][image-apiary] - ---- - ## Self describing APIs The browsable API that REST framework provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser. @@ -221,7 +171,7 @@ If the python `Markdown` library is installed, then [markdown syntax][markdown] [ref]: http://example.com/activating-accounts """ -Note that when using viewsets the basic docstring is used for all generated views. To provide descriptions for each view, such as for the the list and retrieve views, use docstring sections as described in [Schemas as documentation: Examples][schemas-examples]. +Note that when using viewsets the basic docstring is used for all generated views. To provide descriptions for each view, such as for the list and retrieve views, use docstring sections as described in [Schemas as documentation: Examples][schemas-examples]. #### The `OPTIONS` method @@ -253,22 +203,17 @@ In this approach, rather than documenting the available API endpoints up front, To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats. [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 -[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/syntax + [hypermedia-docs]: rest-hypermedia-hateoas.md -[image-django-rest-swagger]: ../img/django-rest-swagger.png -[image-apiary]: ../img/apiary.png -[image-self-describing-api]: ../img/self-describing.png [metadata-docs]: ../api-guide/metadata/ - [schemas-examples]: ../api-guide/schemas/#examples -[swagger-ui]: https://swagger.io/tools/swagger-ui/ -[redoc]: https://github.com/Rebilly/ReDoc +[image-drf-yasg]: ../img/drf-yasg.png +[image-self-describing-api]: ../img/self-describing.png + +[drf-yasg]: https://github.com/axnsan12/drf-yasg/ +[markdown]: https://daringfireball.net/projects/markdown/syntax +[open-api]: https://openapis.org/ +[redoc]: https://github.com/Rebilly/ReDoc +[swagger]: https://swagger.io/ +[swagger-ui]: https://swagger.io/tools/swagger-ui/ diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index 8cbd41c50..482deac66 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,7 +1,7 @@ # PEP8 code linting, which we run on all commits. -flake8==3.5.0 -flake8-tidy-imports==1.1.0 -pycodestyle==2.3.1 +flake8==3.7.8 +flake8-tidy-imports==3.0.0 +pycodestyle==2.5.0 # Sort and lint imports -isort==4.3.3 +isort==4.3.21 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 4d4225c96..2b96e7336 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -10,7 +10,7 @@ ______ _____ _____ _____ __ __title__ = 'Django REST framework' __version__ = '3.10.3' __author__ = 'Tom Christie' -__license__ = 'BSD 2-Clause' +__license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' # Version synonym diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0ceebe3b..ea8f47b2d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1062,9 +1062,7 @@ class DecimalField(Field): except decimal.DecimalException: self.fail('invalid') - # Check for NaN. It is the only value that isn't equal to itself, - # so we can use this to identify NaN values. - if value != value: + if value.is_nan(): self.fail('invalid') # Check for infinity and negative infinity. diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 338776884..9bde79b19 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -47,12 +47,12 @@ class Hyperlink(str): in some contexts, or render as a plain URL in others. """ def __new__(cls, url, obj): - ret = str.__new__(cls, url) + ret = super().__new__(cls, url) ret.obj = obj return ret def __getnewargs__(self): - return(str(self), self.name,) + return (str(self), self.name) @property def name(self): diff --git a/rest_framework/schemas/__init__.py b/rest_framework/schemas/__init__.py index 588680362..b63cb2353 100644 --- a/rest_framework/schemas/__init__.py +++ b/rest_framework/schemas/__init__.py @@ -23,8 +23,8 @@ Other access should target the submodules directly from rest_framework.settings import api_settings from . import coreapi, openapi -from .inspectors import DefaultSchema # noqa from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa +from .inspectors import DefaultSchema # noqa def get_schema_view( diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index d811da2d8..75ed5671a 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -1,26 +1,18 @@ -import re import warnings from collections import Counter, OrderedDict from urllib import parse from django.db import models -from django.utils.encoding import force_str, smart_text +from django.utils.encoding import force_str 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 # - def common_path(paths): split_paths = [path.strip('/').split('/') for path in paths] @@ -397,44 +389,6 @@ class AutoSchema(ViewInspector): 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 diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 77e92eeb8..4b6d82a14 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -151,7 +151,7 @@ class BaseSchemaGenerator(object): # Set by 'SCHEMA_COERCE_PATH_PK'. coerce_path_pk = None - def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=''): + def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None): if url and not url.endswith('/'): url += '/' diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 86fcdc435..3b7e7f963 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -3,9 +3,13 @@ inspectors.py # Per-endpoint view introspection See schemas.__init__.py for package overview. """ +import re from weakref import WeakKeyDictionary +from django.utils.encoding import smart_text + from rest_framework.settings import api_settings +from rest_framework.utils import formatting class ViewInspector: @@ -15,6 +19,9 @@ class ViewInspector: Provide subclass for per-view schema generation """ + # Used in _get_description_section() + header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') + def __init__(self): self.instance_schemas = WeakKeyDictionary() @@ -62,6 +69,45 @@ class ViewInspector: def view(self): self._view = None + def get_description(self, path, method): + """ + Determine a path 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 self.header_regex.match(line): + current_section, separator, 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() + class DefaultSchema(ViewInspector): """Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting""" diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 09a5598f5..134df5043 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -1,4 +1,5 @@ import warnings +from operator import attrgetter from urllib.parse import urljoin from django.core.validators import ( @@ -8,7 +9,7 @@ from django.core.validators import ( from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers +from rest_framework import exceptions, renderers, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty @@ -16,15 +17,14 @@ 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): + # Title and version are required by openapi specification 3.x info = { - 'title': self.title, - 'version': self.version, + 'title': self.title or '', + 'version': self.version or '' } if self.description is not None: @@ -78,7 +78,9 @@ class SchemaGenerator(BaseSchemaGenerator): class AutoSchema(ViewInspector): - content_types = ['application/json'] + request_media_types = [] + response_media_types = [] + method_mapping = { 'get': 'Retrieve', 'post': 'Create', @@ -91,6 +93,7 @@ class AutoSchema(ViewInspector): operation = {} operation['operationId'] = self._get_operation_id(path, method) + operation['description'] = self.get_description(path, method) parameters = [] parameters += self._get_path_parameters(path, method) @@ -209,7 +212,7 @@ class AutoSchema(ViewInspector): if not is_list_view(path, method, view): return [] - paginator = self._get_pagninator() + paginator = self._get_paginator() if not paginator: return [] @@ -265,9 +268,13 @@ class AutoSchema(ViewInspector): 'items': {}, } if not isinstance(field.child, _UnvalidatedField): - mapping['items'] = { - "type": self._map_field(field.child).get('type') + map_field = self._map_field(field.child) + items = { + "type": map_field.get('type') } + if 'format' in map_field: + items['format'] = map_field.get('format') + mapping['items'] = items return mapping # DateField and DateTimeField type is string @@ -337,13 +344,23 @@ class AutoSchema(ViewInspector): 'type': 'integer' } self._map_min_max(field, content) + # 2147483647 is max for int32_size, so we use int64 for format + if int(content.get('maximum', 0)) > 2147483647 or int(content.get('minimum', 0)) > 2147483647: + content['format'] = 'int64' return content + if isinstance(field, serializers.FileField): + return { + 'type': 'string', + 'format': 'binary' + } + # Simplest cases, default to 'string' type: FIELD_CLASS_SCHEMA_TYPE = { serializers.BooleanField: 'boolean', serializers.JSONField: 'object', serializers.DictField: 'object', + serializers.HStoreField: 'object', } return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')} @@ -429,13 +446,24 @@ class AutoSchema(ViewInspector): schema['maximum'] = int(digits * '9') + 1 schema['minimum'] = -schema['maximum'] - def _get_pagninator(self): + def _get_paginator(self): pagination_class = getattr(self.view, 'pagination_class', None) if pagination_class: return pagination_class() - return None + def map_parsers(self, path, method): + return list(map(attrgetter('media_type'), self.view.parser_classes)) + + def map_renderers(self, path, method): + media_types = [] + for renderer in self.view.renderer_classes: + # BrowsableAPIRenderer not relevant to OpenAPI spec + if renderer == renderers.BrowsableAPIRenderer: + continue + media_types.append(renderer.media_type) + return media_types + def _get_serializer(self, method, path): view = self.view @@ -455,6 +483,8 @@ class AutoSchema(ViewInspector): if method not in ('PUT', 'PATCH', 'POST'): return {} + self.request_media_types = self.map_parsers(path, method) + serializer = self._get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): @@ -472,7 +502,7 @@ class AutoSchema(ViewInspector): return { 'content': { ct: {'schema': content} - for ct in self.content_types + for ct in self.request_media_types } } @@ -485,6 +515,8 @@ class AutoSchema(ViewInspector): } } + self.response_media_types = self.map_renderers(path, method) + item_schema = {} serializer = self._get_serializer(path, method) @@ -502,7 +534,7 @@ class AutoSchema(ViewInspector): 'type': 'array', 'items': item_schema, } - paginator = self._get_pagninator() + paginator = self._get_paginator() if paginator: response_schema = paginator.get_paginated_response_schema(response_schema) else: @@ -512,7 +544,7 @@ class AutoSchema(ViewInspector): '200': { 'content': { ct: {'schema': response_schema} - for ct in self.content_types + for ct in self.response_media_types }, # description is a mandatory property, # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject diff --git a/rest_framework/static/rest_framework/js/bootstrap.min.js b/rest_framework/static/rest_framework/js/bootstrap.min.js index 4cd821990..eb0a8b410 100644 --- a/rest_framework/static/rest_framework/js/bootstrap.min.js +++ b/rest_framework/static/rest_framework/js/bootstrap.min.js @@ -1,6 +1,6 @@ /*! - * Bootstrap v3.4.0 (https://getbootstrap.com/) - * Copyright 2011-2018 Twitter, Inc. + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. * Licensed under the MIT license */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3this.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){return(t=g.extend({},this.getDefaults(),this.$element.data(),t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})};var t=g.fn.tooltip;g.fn.tooltip=function e(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=t,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.0",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof i?"html":"append":"text"](i),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.0",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.0",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return nthis.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return n