Compare commits

..

16 Commits

Author SHA1 Message Date
Joe Korbel 957b4b125e Update openapi.rst (#327)
Minor typo on "because"
2019-03-08 18:24:55 +02:00
Cristi Vîjdea a59f632516 Add OAS 3 types
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md
2018-09-16 18:47:55 +03:00
Cristi Vîjdea 20786e53c3 Provide more OpenAPISchemaGenerator extension points
Add ``should_include_endpoint`` and ``get_paths_object``.
2018-09-13 05:51:34 +03:00
Cristi Vîjdea 120c4dd884 Fix IPv4 pattern on Django 1.11 2018-09-13 05:14:08 +03:00
Cristi Vîjdea 5bdce56bba Fix docs build with sphinx 1.8 2018-09-13 05:12:40 +03:00
Cristi Vîjdea e761e2da50 Add 1.10.2 changelog 2018-09-13 04:19:31 +03:00
Cristi Vîjdea 9d330d9509 Use the first line of view method docstring as Operation summary
Closes #205.
2018-09-13 04:18:55 +03:00
Cristi Vîjdea c8f6d16891 Update ReDoc to 2.0.0-alpha.38 2018-09-13 03:44:10 +03:00
Cristi Vîjdea 857ef05b07 Handle reverse_lazy
Fixes #209.
2018-09-13 03:38:13 +03:00
Cristi Vîjdea 586b88d8f3 Allow pattern on any field 2018-09-13 03:34:01 +03:00
Cristi Vîjdea 27cd5f7f2a Add some convenience methods 2018-09-11 02:41:55 +03:00
Cristi Vîjdea 90812f5c43 Refactor get_queryset_from_view 2018-09-10 14:34:30 +03:00
Cristi Vîjdea a6ae8b0521 Add ReDoc settings descriptions 2018-09-10 14:02:37 +03:00
Cristi Vîjdea 91530b64c6 Add DISPLAY_OPERATION_ID setting for swagger-ui
Closes #206.
2018-09-10 14:02:25 +03:00
Cristi Vîjdea 6820d6bc70 Fix RelatedFieldInspector nesting issues 2018-09-10 05:04:03 +03:00
Cristi Vîjdea f50ad29a2b Fix lint errors 2018-09-10 02:48:08 +03:00
23 changed files with 1220 additions and 70 deletions
+2 -3
View File
@@ -1,7 +1,8 @@
node_modules/ node_modules/
testproj/db.sqlite3 testproj/db.sqlite3
testproj/staticfiles testproj/staticfiles
.vscode/ \.pytest_cache/
docs/\.doctrees/
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Python template ### Python template
@@ -159,5 +160,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
\.pytest_cache/
+1
View File
@@ -18,6 +18,7 @@
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.pytest_cache" /> <excludeFolder url="file://$MODULE_DIR$/.pytest_cache" />
<excludeFolder url="file://$MODULE_DIR$/dist" /> <excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/docs/.doctrees" />
<excludeFolder url="file://$MODULE_DIR$/docs/_build" /> <excludeFolder url="file://$MODULE_DIR$/docs/_build" />
<excludeFolder url="file://$MODULE_DIR$/htmlcov" /> <excludeFolder url="file://$MODULE_DIR$/htmlcov" />
<excludeFolder url="file://$MODULE_DIR$/src/drf_yasg.egg-info" /> <excludeFolder url="file://$MODULE_DIR$/src/drf_yasg.egg-info" />
-5
View File
@@ -55,11 +55,6 @@
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="ignoreTupleUnpacking" value="true" />
<option name="ignoreLambdaParameters" value="true" />
<option name="ignoreLoopIterationVariables" value="true" />
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
+15 -1
View File
@@ -3,6 +3,20 @@ Changelog
######### #########
**********
**1.10.2**
**********
*Release date: Sep 13, 2018*
- **ADDED:** added the ``DISPLAY_OPERATION_ID `` ``swagger-ui`` setting
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-alpha.38
- **IMPROVED:** Operation summary will now be parsed from multi-line view method docstrings (:issue:`205`)
- **IMPROVED:** ``pattern`` will now work on any field with a ``RegexValidator``
(would previously not appear on fields with special formats such as ``EmailField``)
- **FIXED:** fixed an issue with ``RelatedFieldInspector`` handling of nested serializers
- **FIXED:** fixed handling of ``reverse_lazy`` in URL settings (:issue:`209`)
********** **********
**1.10.1** **1.10.1**
********** **********
@@ -19,7 +33,7 @@ Changelog
- **FIXED:** fixed implementation of ``operation_summary`` and ``deprecated`` (:pr:`194`, :pr:`198`) - **FIXED:** fixed implementation of ``operation_summary`` and ``deprecated`` (:pr:`194`, :pr:`198`)
- **FIXED:** fixed a bug related to nested ``typing`` hints (:pr:`195`) - **FIXED:** fixed a bug related to nested ``typing`` hints (:pr:`195`)
- **FIXED:** removed dependency on ``future`` (:issue:`196`) - **FIXED:** removed dependency on ``future`` (:issue:`196`)
- **FIXED:** removed exceptions loged for fields with ``default=None`` (:issue:`203`) - **FIXED:** fixed exceptions logged for fields with ``default=None`` (:issue:`203`)
- **FIXED:** fixed ``request_body=no_body`` handling and related tests (:issue:`188`, :issue:`199`) - **FIXED:** fixed ``request_body=no_body`` handling and related tests (:issue:`188`, :issue:`199`)
+3 -1
View File
@@ -155,7 +155,9 @@ texinfo_documents = [
'Miscellaneous'), 'Miscellaneous'),
] ]
autodoc_default_flags = ['private-members'] autodoc_default_options = {
'private-members': None
}
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
autoclass_content = 'both' autoclass_content = 'both'
autodoc_mock_imports = [] autodoc_mock_imports = []
+1 -1
View File
@@ -14,7 +14,7 @@ This library generates OpenAPI 2.0 documents. The authoritative specification fo
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__. page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification Because the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
is structured, starting from the root ``Swagger`` object. is structured, starting from the root ``Swagger`` object.
* :class:`.Swagger` object * :class:`.Swagger` object
+24
View File
@@ -332,6 +332,14 @@ This does not filter the operations from the display.
**Default**: :python:`['get','put','post','delete','options','head','patch','trace']` |br| **Default**: :python:`['get','put','post','delete','options','head','patch','trace']` |br|
*Maps to parameter*: ``supportedSubmitMethods`` *Maps to parameter*: ``supportedSubmitMethods``
DISPLAY_OPERATION_ID
--------------------
Controls the display of operationId in operations list.
**Default**: :python:`True`
*Maps to parameter*: ``displayOperationId``
************** **************
REDOC_SETTINGS REDOC_SETTINGS
************** **************
@@ -356,6 +364,10 @@ URL which serves the UI; see :ref:`note on URL settings <url-settings>` above.
LAZY_RENDERING LAZY_RENDERING
-------------- --------------
If set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50).
In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing
progress bar on the top.
**NOTE:** this feature might be removed in future versions of ReDoc (see https://github.com/Rebilly/ReDoc/issues/475) **NOTE:** this feature might be removed in future versions of ReDoc (see https://github.com/Rebilly/ReDoc/issues/475)
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
@@ -364,30 +376,42 @@ LAZY_RENDERING
HIDE_HOSTNAME HIDE_HOSTNAME
------------- -------------
If set, the protocol and hostname is not shown in the operation definition.
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
*Maps to attribute*: ``hideHostname`` *Maps to attribute*: ``hideHostname``
EXPAND_RESPONSES EXPAND_RESPONSES
---------------- ----------------
Specify which responses to expand by default by response codes. Values should be passed as comma-separated list without
spaces e.g. expandResponses="200,201". Special value "all" expands all responses by default.
Be careful: this option can slow-down documentation rendering time.
**Default**: :python:`'all'` |br| **Default**: :python:`'all'` |br|
*Maps to attribute*: ``expandResponses`` *Maps to attribute*: ``expandResponses``
PATH_IN_MIDDLE PATH_IN_MIDDLE
-------------- --------------
Show path link and HTTP verb in the middle panel instead of the right one.
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
*Maps to attribute*: ``pathInMiddlePanel`` *Maps to attribute*: ``pathInMiddlePanel``
NATIVE_SCROLLBARS NATIVE_SCROLLBARS
----------------- -----------------
Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
*Maps to attribute*: ``nativeScrollbars`` *Maps to attribute*: ``nativeScrollbars``
REQUIRED_PROPS_FIRST REQUIRED_PROPS_FIRST
-------------------- --------------------
Show required properties first ordered in the same order as in required array.
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
*Maps to attribute*: ``requiredPropsFirst`` *Maps to attribute*: ``requiredPropsFirst``
+26 -26
View File
@@ -72,11 +72,11 @@
} }
}, },
"debug": { "debug": {
"version": "3.1.0", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"requires": { "requires": {
"ms": "2.0.0" "ms": "^2.1.1"
} }
}, },
"decko": { "decko": {
@@ -272,18 +272,18 @@
} }
}, },
"mobx-react": { "mobx-react": {
"version": "5.2.6", "version": "5.2.8",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.2.6.tgz", "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.2.8.tgz",
"integrity": "sha512-AGo3m/ICzN2SEbtNi8ZdF1ShDcAOnEKonZFc3mm8uuF2obDXg6rXqV5mtkr9ZHPYizGp51kT/vRg0TlGzok+CA==", "integrity": "sha512-VAkeqkrIpdoy3VPPoqvxjdQmcTT6hi7i3TsZSwcKbSrPbSTuWc7cp1EOiX/zV1wPWEuoNAQ22bCrskQwvdYTJA==",
"requires": { "requires": {
"hoist-non-react-statics": "^2.5.0", "hoist-non-react-statics": "^2.5.0",
"react-lifecycles-compat": "^3.0.2" "react-lifecycles-compat": "^3.0.2"
} }
}, },
"ms": { "ms": {
"version": "2.0.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}, },
"node-fetch": { "node-fetch": {
"version": "1.7.3", "version": "1.7.3",
@@ -321,9 +321,9 @@
"integrity": "sha512-/2Sk/khljhdrsamjJYS5NjrH+GKEHEwh7zFSiYyxROyYKagkE4kSn2zDQDRTOMo8mpT2jikxx6yI1dG7lNP/hw==" "integrity": "sha512-/2Sk/khljhdrsamjJYS5NjrH+GKEHEwh7zFSiYyxROyYKagkE4kSn2zDQDRTOMo8mpT2jikxx6yI1dG7lNP/hw=="
}, },
"polished": { "polished": {
"version": "1.9.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/polished/-/polished-1.9.3.tgz", "resolved": "https://registry.npmjs.org/polished/-/polished-2.0.3.tgz",
"integrity": "sha512-4NmSD7fMFlM8roNxs7YXPv7UFRbYzb0gufR5zBxJLRzY54+zFsavxBo6zsQzP9ep6Hh3pC2pTyrpSTBEaB6IkQ==" "integrity": "sha512-MMwthxXsmZZlZY/x79KX/8kXpSU0lq3ntnDmAdhpQGk9pU9EM/rQmUbsgIuuk2Cvl6zYSWjyBsDEU8sa1598xg=="
}, },
"postcss-value-parser": { "postcss-value-parser": {
"version": "3.3.0", "version": "3.3.0",
@@ -401,9 +401,9 @@
} }
}, },
"redoc": { "redoc": {
"version": "2.0.0-alpha.37", "version": "2.0.0-alpha.38",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-alpha.37.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-alpha.38.tgz",
"integrity": "sha512-yuT8sTT+oNzg8G9PfsUhu8Jhd9rWYJCTSQnbgaq9KzAFcJ2O0XfvlstkGXDK1zmtSSBBe5cy9Z/1XcQGDiGYug==", "integrity": "sha512-4pT/cRJ/aKdLlCxjFOSWvkA9ZVG5b9dUfxIM+H5698hlRaF6rcu4qh29eGwEikSDw0unH0iNd6MHy9u0fAjRGw==",
"requires": { "requires": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"decko": "^1.2.0", "decko": "^1.2.0",
@@ -418,15 +418,15 @@
"mobx-react": "^5.2.5", "mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.14", "openapi-sampler": "1.0.0-beta.14",
"perfect-scrollbar": "^1.4.0", "perfect-scrollbar": "^1.4.0",
"polished": "^1.9.3", "polished": "^2.0.2",
"prismjs": "^1.15.0", "prismjs": "^1.15.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-dropdown": "^1.6.1", "react-dropdown": "^1.6.2",
"react-hot-loader": "^4.3.4", "react-hot-loader": "^4.3.5",
"react-tabs": "^2.0.0", "react-tabs": "^2.0.0",
"slugify": "^1.3.1", "slugify": "^1.3.1",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"styled-components": "^3.4.2", "styled-components": "^3.4.5",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
@@ -467,9 +467,9 @@
"integrity": "sha1-OUE/7p0CXHSn5ZzuyyN4TMDxfwI=" "integrity": "sha1-OUE/7p0CXHSn5ZzuyyN4TMDxfwI="
}, },
"styled-components": { "styled-components": {
"version": "3.4.5", "version": "3.4.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-3.4.5.tgz", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-3.4.6.tgz",
"integrity": "sha512-/4c4/72+QAZ8LO+VIzTcitdjMcJleOln1laK9HZr166O+OmGpKZYt3+hRn0YhYPytwDGfx9J5c7WhU7Oys+nSw==", "integrity": "sha512-HiEVVY93oH/PuekPuIE7ownn2wmO708rS8MM1yS20i0TCyFgPs3mbop8fJqKLRtC2/npVn2fO5Je6azaL6T2tg==",
"requires": { "requires": {
"buffer": "^5.0.3", "buffer": "^5.0.3",
"css-to-react-native": "^2.0.3", "css-to-react-native": "^2.0.3",
@@ -522,9 +522,9 @@
"integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==" "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA=="
}, },
"whatwg-fetch": { "whatwg-fetch": {
"version": "2.0.4", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
} }
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "drf-yasg", "name": "drf-yasg",
"dependencies": { "dependencies": {
"redoc": "^2.0.0-alpha.37", "redoc": "^2.0.0-alpha.38",
"swagger-ui-dist": "^3.18.2" "swagger-ui-dist": "^3.18.2"
}, },
"repository": { "repository": {
+1
View File
@@ -63,6 +63,7 @@ SWAGGER_DEFAULTS = {
'patch', 'patch',
'trace' 'trace'
], ],
'DISPLAY_OPERATION_ID': True,
} }
REDOC_DEFAULTS = { REDOC_DEFAULTS = {
+25 -4
View File
@@ -15,7 +15,7 @@ from rest_framework.settings import api_settings as rest_framework_settings
from . import openapi from . import openapi
from .app_settings import swagger_settings from .app_settings import swagger_settings
from .errors import SwaggerGenerationError from .errors import SwaggerGenerationError
from .inspectors.field import get_basic_type_info, get_queryset_field from .inspectors.field import get_basic_type_info, get_queryset_field, get_queryset_from_view
from .openapi import ReferenceResolver from .openapi import ReferenceResolver
from .utils import force_real_str, get_consumes, get_produces from .utils import force_real_str, get_consumes, get_produces
@@ -311,6 +311,27 @@ class OpenAPISchemaGenerator(object):
""" """
return self._gen.determine_path_prefix(paths) return self._gen.determine_path_prefix(paths)
def should_include_endpoint(self, path, method, view, public):
"""Check if a given endpoint should be included in the resulting schema.
:param str path: request path
:param str method: http request method
:param view: instantiated view callback
:param bool public: if True, all endpoints are included regardless of access through `request`
:returns: true if the view should be excluded
:rtype: bool
"""
return public or self._gen.has_view_permissions(path, method, view)
def get_paths_object(self, paths):
"""Construct the Swagger Paths object.
:param OrderedDict[str,openapi.PathItem] paths: mapping of paths to :class:`.PathItem` objects
:returns: the :class:`.Paths` object
:rtype: openapi.Paths
"""
return openapi.Paths(paths=paths)
def get_paths(self, endpoints, components, request, public): def get_paths(self, endpoints, components, request, public):
"""Generate the Swagger Paths for the API from the given endpoints. """Generate the Swagger Paths for the API from the given endpoints.
@@ -331,7 +352,7 @@ class OpenAPISchemaGenerator(object):
for path, (view_cls, methods) in sorted(endpoints.items()): for path, (view_cls, methods) in sorted(endpoints.items()):
operations = {} operations = {}
for method, view in methods: for method, view in methods:
if not public and not self._gen.has_view_permissions(path, method, view): if not self.should_include_endpoint(path, method, view, public):
continue continue
operation = self.get_operation(view, path, prefix, method, components, request) operation = self.get_operation(view, path, prefix, method, components, request)
@@ -346,7 +367,7 @@ class OpenAPISchemaGenerator(object):
path_suffix = '/' + path_suffix path_suffix = '/' + path_suffix
paths[path_suffix] = self.get_path_item(path, view_cls, operations) paths[path_suffix] = self.get_path_item(path, view_cls, operations)
return openapi.Paths(paths=paths), prefix return self.get_paths_object(paths), prefix
def get_operation(self, view, path, prefix, method, components, request): def get_operation(self, view, path, prefix, method, components, request):
"""Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to """Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to
@@ -424,7 +445,7 @@ class OpenAPISchemaGenerator(object):
:rtype: list[openapi.Parameter] :rtype: list[openapi.Parameter]
""" """
parameters = [] parameters = []
queryset = getattr(view_cls, 'queryset', None) queryset = get_queryset_from_view(view_cls)
for variable in sorted(uritemplate.variables(path)): for variable in sorted(uritemplate.variables(path)):
model, model_field = get_queryset_field(queryset, variable) model, model_field = get_queryset_field(queryset, variable)
+35 -4
View File
@@ -165,6 +165,25 @@ def get_model_field(model, field_name):
return None return None
def get_queryset_from_view(view, serializer=None):
"""Try to get the queryset of the given view
:param view: the view instance or class
:param serializer: if given, will check that the view's get_serializer_class return matches this serialzier
:return: queryset or ``None``
"""
try:
queryset = getattr(view, 'queryset', None)
if queryset is not None and serializer is not None:
# make sure the view is actually using *this* serializer
assert type(serializer) == view.get_serializer_class()
return queryset
except Exception: # pragma: no cover
return None
def get_parent_serializer(field): def get_parent_serializer(field):
"""Get the nearest parent ``Serializer`` instance for the given field. """Get the nearest parent ``Serializer`` instance for the given field.
@@ -231,10 +250,18 @@ class RelatedFieldInspector(FieldInspector):
else: else:
# if the RelatedField has no queryset (e.g. read only), try to find the target model # if the RelatedField has no queryset (e.g. read only), try to find the target model
# from the view queryset or ModelSerializer model, if present # from the view queryset or ModelSerializer model, if present
view_queryset = getattr(self.view, 'queryset', None) parent_serializer = get_parent_serializer(field)
serializer_meta = getattr(get_parent_serializer(field), 'Meta', None)
this_model = getattr(view_queryset, 'model', None) or getattr(serializer_meta, 'model', None) serializer_meta = getattr(parent_serializer, 'Meta', None)
this_model = getattr(serializer_meta, 'model', None)
if not this_model:
view_queryset = get_queryset_from_view(self.view, parent_serializer)
this_model = getattr(view_queryset, 'model', None)
source = getattr(field, 'source', '') or field.field_name source = getattr(field, 'source', '') or field.field_name
if not source and isinstance(field.parent, serializers.ManyRelatedField):
source = field.parent.field_name
model = get_related_model(this_model, source) model = get_related_model(this_model, source)
model_field = get_model_field(model, target_field) model_field = get_model_field(model, target_field)
@@ -256,6 +283,10 @@ def find_regex(regex_field):
regex_validator = None regex_validator = None
for validator in regex_field.validators: for validator in regex_field.validators:
if isinstance(validator, validators.RegexValidator): if isinstance(validator, validators.RegexValidator):
if isinstance(validator, validators.URLValidator) or validator == validators.validate_ipv4_address:
# skip the default url and IP regexes because they are complex and unhelpful
# validate_ipv4_address is a RegexValidator instance in Django 1.11
continue
if regex_validator is not None: if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose # bail if multiple validators are found - no obvious way to choose
return None # pragma: no cover return None # pragma: no cover
@@ -401,7 +432,7 @@ def get_basic_type_info(field):
return None return None
pattern = None pattern = None
if swagger_type == openapi.TYPE_STRING and format in (None, openapi.FORMAT_SLUG): if swagger_type == openapi.TYPE_STRING:
pattern = find_regex(field) pattern = find_regex(field)
limits = find_limits(field) limits = find_limits(field)
+25 -11
View File
@@ -8,8 +8,8 @@ from rest_framework.status import is_success
from .. import openapi from .. import openapi
from ..errors import SwaggerGenerationError from ..errors import SwaggerGenerationError
from ..utils import ( from ..utils import (
force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status, is_list_view, no_body, force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status, is_list_view,
param_list_to_odict merge_params, no_body, param_list_to_odict
) )
from .base import ViewInspector from .base import ViewInspector
@@ -162,21 +162,19 @@ class SwaggerAutoSchema(ViewInspector):
:return: modified parameters :return: modified parameters
:rtype: list[openapi.Parameter] :rtype: list[openapi.Parameter]
""" """
parameters = param_list_to_odict(parameters)
manual_parameters = self.overrides.get('manual_parameters', None) or [] manual_parameters = self.overrides.get('manual_parameters', None) or []
if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover
raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body") raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")
if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover
if any(param.in_ == openapi.IN_BODY for param in parameters.values()): if any(param.in_ == openapi.IN_BODY for param in parameters):
raise SwaggerGenerationError("cannot add form parameters when the request has a request body; " raise SwaggerGenerationError("cannot add form parameters when the request has a request body; "
"did you forget to set an appropriate parser class on the view?") "did you forget to set an appropriate parser class on the view?")
if self.method not in self.body_methods: if self.method not in self.body_methods:
raise SwaggerGenerationError("form parameters can only be applied to (" + ','.join(self.body_methods) + raise SwaggerGenerationError("form parameters can only be applied to (" + ','.join(self.body_methods) +
") HTTP methods") ") HTTP methods")
parameters.update(param_list_to_odict(manual_parameters)) return merge_params(parameters, manual_parameters)
return list(parameters.values())
def get_responses(self): def get_responses(self):
"""Get the possible responses for this view as a swagger :class:`.Responses` object. """Get the possible responses for this view as a swagger :class:`.Responses` object.
@@ -323,16 +321,32 @@ class SwaggerAutoSchema(ViewInspector):
operation_id = '_'.join(operation_keys) operation_id = '_'.join(operation_keys)
return operation_id return operation_id
def _extract_description_and_summary(self):
description = self.overrides.get('operation_description', None)
summary = self.overrides.get('operation_summary', None)
if description is None:
description = self._sch.get_description(self.path, self.method) or ''
description = description.strip().replace('\r', '')
if description and (summary is None):
# description from docstring ... do summary magic
# https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings
summary_max_len = 120 # OpenAPI 2.0 spec says summary should be under 120 characters
sections = description.split('\n\n', 1)
if len(sections) == 2:
sections[0] = sections[0].strip()
if len(sections[0]) < summary_max_len:
summary, description = sections
return description, summary
def get_description(self): def get_description(self):
"""Return an operation description determined as appropriate from the view's method and class docstrings. """Return an operation description determined as appropriate from the view's method and class docstrings.
:return: the operation description :return: the operation description
:rtype: str :rtype: str
""" """
description = self.overrides.get('operation_description', None) return self._extract_description_and_summary()[0]
if description is None:
description = self._sch.get_description(self.path, self.method)
return description
def get_summary(self): def get_summary(self):
"""Return a summary description for this operation. """Return a summary description for this operation.
@@ -340,7 +354,7 @@ class SwaggerAutoSchema(ViewInspector):
:return: the summary :return: the summary
:rtype: str :rtype: str
""" """
return self.overrides.get('operation_summary', None) return self._extract_description_and_summary()[1]
def get_security(self): def get_security(self):
"""Return a list of security requirements for this operation. """Return a list of security requirements for this operation.
+11
View File
@@ -291,6 +291,8 @@ class Paths(SwaggerDict):
class PathItem(SwaggerDict): class PathItem(SwaggerDict):
OPERATION_NAMES = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']
def __init__(self, get=None, put=None, post=None, delete=None, options=None, def __init__(self, get=None, put=None, post=None, delete=None, options=None,
head=None, patch=None, parameters=None, **extra): head=None, patch=None, parameters=None, **extra):
"""Information about a single path """Information about a single path
@@ -315,6 +317,15 @@ class PathItem(SwaggerDict):
self.parameters = filter_none(parameters) self.parameters = filter_none(parameters)
self._insert_extras__() self._insert_extras__()
@property
def operations(self):
"""A list of all standard Operations on this PathItem object. See :attr:`.OPERATION_NAMES`.
:return: list of (method name, Operation) tuples
:rtype: list[tuple[str,Operation]]
"""
return [(k, v) for k, v in self.items() if k in PathItem.OPERATION_NAMES and v]
class Operation(SwaggerDict): class Operation(SwaggerDict):
def __init__(self, operation_id, responses, parameters=None, consumes=None, produces=None, summary=None, def __init__(self, operation_id, responses, parameters=None, consumes=None, produces=None, summary=None,
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,8 +1,7 @@
import warnings
import six import six
from django.shortcuts import render, resolve_url from django.shortcuts import render, resolve_url
from django.utils.functional import Promise
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import json from rest_framework.utils import json
@@ -83,6 +82,9 @@ class _UIRenderer(BaseRenderer):
renderer_context.update(self.get_auth_urls()) renderer_context.update(self.get_auth_urls())
def resolve_url(self, to): def resolve_url(self, to):
if isinstance(to, Promise):
to = str(to)
if to is None: if to is None:
return None return None
@@ -135,6 +137,7 @@ class SwaggerUIRenderer(_UIRenderer):
'showCommonExtensions': swagger_settings.SHOW_COMMON_EXTENSIONS, 'showCommonExtensions': swagger_settings.SHOW_COMMON_EXTENSIONS,
'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL, 'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL,
'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS, 'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS,
'displayOperationId': swagger_settings.DISPLAY_OPERATION_ID,
} }
data = filter_none(data) data = filter_none(data)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -32,7 +32,6 @@ function initSwaggerUi() {
var swaggerConfig = { var swaggerConfig = {
url: specURL, url: specURL,
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
displayOperationId: true,
displayRequestDuration: true, displayRequestDuration: true,
presets: [ presets: [
SwaggerUIBundle.presets.apis, SwaggerUIBundle.presets.apis,
+16
View File
@@ -254,6 +254,22 @@ def param_list_to_odict(parameters):
return result return result
def merge_params(parameters, overrides):
"""Merge `overrides` into `parameters`. This is the same as appending `overrides` to `parameters`, but any element
of `parameters` whose ``(name, in_)`` tuple collides with an element in `overrides` is replaced by it.
Raises an ``AssertionError`` if either list contains duplicate parameters.
:param list[.Parameter] parameters: initial parameters
:param list[.Parameter] overrides: overriding parameters
:return: merged list
:rtype: list[.Parameter]
"""
parameters = param_list_to_odict(parameters)
parameters.update(param_list_to_odict(overrides))
return list(parameters.values())
def filter_none(obj): def filter_none(obj):
"""Remove ``None`` values from tuples, lists or dictionaries. Return other objects as-is. """Remove ``None`` values from tuples, lists or dictionaries. Return other objects as-is.
+4
View File
@@ -46,6 +46,10 @@ class SnippetList(generics.ListCreateAPIView):
), ),
) )
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""summary from docstring
description body is here, summary is not included
"""
pass pass
+3 -1
View File
@@ -1,6 +1,8 @@
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
from django.urls import reverse_lazy
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
@@ -91,7 +93,7 @@ REST_FRAMEWORK = {
# drf-yasg # drf-yasg
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': reverse_lazy('admin:login'),
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',
'DEFAULT_INFO': 'testproj.urls.swagger_info', 'DEFAULT_INFO': 'testproj.urls.swagger_info',
+2 -1
View File
@@ -391,7 +391,8 @@ paths:
- snippets - snippets
delete: delete:
operationId: snippetsDeleteBulk operationId: snippetsDeleteBulk
description: SnippetList classdoc summary: summary from docstring
description: description body is here, summary is not included
parameters: parameters:
- name: data - name: data
in: body in: body