Compare commits

..

70 Commits

Author SHA1 Message Date
Cristi Vîjdea 6b2ce7d0f9 Update changelog and release 1.8.0 2018-05-31 00:24:48 +03:00
Cristi Vîjdea 256a052564 Add ability to set Schema fields through the serializer Meta class (#134)
* Add swagger_schema_fields attribute to serializer Meta class
* Add documentation

Closes #132.
2018-05-31 00:15:21 +03:00
Cristi Vîjdea cc90bc1544 Add some coverage exemptions 2018-05-30 22:33:36 +03:00
Terence Honles ecee6f8177 apply fix from #58 to _SpecRenderer (#130)
* apply fix from #58 to _SpecRenderer
* Use JSONRenderer instead of HTML
2018-05-30 22:03:00 +03:00
werwty 408b31fc4f Avoid marking read_only fields as required (#133)
* Avoid marking read_only fields as required

Read only properties cannot be marked as required by a schema.
2018-05-30 21:56:52 +03:00
Cristi Vîjdea a4a11ad1ab Prevent crash when ViewInspector.get_operation returns None 2018-05-14 22:10:13 +03:00
Cristi Vîjdea aca0c4713e Allow body on HTTP DELETE view methods (#122)
* Allow body in delete requests
* Do not add request body to DELETE by default
* Check manual form parameters against body_methods
* Add tests
* Add changelog

Closes #118
2018-05-14 19:15:14 +03:00
Cristi Vîjdea 3077195396 Update swagger-ui to 3.14.2 and ReDoc to 2.0.0-alpha.20 2018-05-14 18:59:59 +03:00
Cristi Vîjdea 94f6ca8c89 Ignore python tests in node_modules 2018-05-14 18:57:29 +03:00
Cristi Vîjdea ae5eeeb600 Ignore None return from get_operation 2018-05-14 18:36:44 +03:00
Cristi Vîjdea 23ebe2ff3e Guard against views that throw exceptions from __init__ 2018-05-12 18:14:33 +03:00
Cristi Vîjdea 713c669e28 Fix changelog syntax... 2018-05-12 14:26:23 +03:00
Cristi Vîjdea 2545ae9657 Fix SECURITY_REQUIREMENT order (again)
pain in the ass -.-
2018-05-12 14:11:37 +03:00
Cristi Vîjdea f03d9d71e9 Fix SECURITY_DEFINITIONS and SECURITY_REQUIREMENTS ordering 2018-05-12 13:54:43 +03:00
Cristi Vîjdea a993cba7aa Fix default security requirements 2018-05-12 13:35:17 +03:00
Cristi Vîjdea bfd88cbdb4 Fix security type hint 2018-05-12 13:30:57 +03:00
Cristi Vîjdea 123a05c82c Fix 1.7.1 changelog 2018-05-05 15:58:10 +03:00
Cristi Vîjdea 7065429d47 Remove python 2.7 + DRF 3.8 build from Travis CI jobs 2018-05-05 15:53:40 +03:00
Cristi Vîjdea bd727fbe88 Update swagger-ui to 3.14.1
Add settings key for ``showCommonExtensions``
2018-05-05 15:33:38 +03:00
Cristi Vîjdea 698a175a1b Remove python_requires from Django requirement
Fixes #113.
2018-05-02 20:30:09 +03:00
Fabian Weisshaar 5b225423ae Set min_length=1 when allow_blank=False (#112) 2018-04-27 13:02:26 +03:00
Cristi Vîjdea 209201b9a5 Make insertion order of SwaggerDict extra parameters consistent 2018-04-27 12:12:36 +03:00
Cristi Vîjdea ca00ed35be Install pytest-django from PyPI 2018-04-27 12:04:20 +03:00
Cristi Vîjdea 4e7fa28744 Superficial fixes 2018-04-27 01:53:05 +03:00
Roman Sichny 979ec84630 Django rest framework recursive support (#110)
* add get_serializer_ref_name utility function
* implement RecursiveFieldInspector
* add option to allow non-existing reference in SchemaRef
* add examples and README
* Update changelog and docs
2018-04-27 01:51:10 +03:00
Cristi Vîjdea d2dc09cb3c Fix changelog markup 2018-04-25 23:04:43 +03:00
Cristi Vîjdea 36992072ca Add 1.6.2 changelog 2018-04-25 22:58:02 +03:00
Cristi Vîjdea aa90723703 Switch to redoc 2.0 alpha
Update redoc to 2.0.0-alpha.17
2018-04-25 22:55:25 +03:00
Cristi Vîjdea 301511bebc Update swagger-ui to 3.13.6 2018-04-25 22:44:49 +03:00
Cristi Vîjdea c0f37f6ec1 Avoid crash when hitting unknown model fields
Fixes #102
2018-04-25 22:41:06 +03:00
Tobias Krönke 5e684549a1 Correct values in swagger-ui docs (#108) 2018-04-25 17:53:22 +03:00
John Carter 322971f3e7 Make parameters order stable for multi-parameter URLs (#106)
Fixes #105

* Added intermittently failing test for #105
* Make parameter order stable for urls with multiple params
2018-04-23 19:16:22 +03:00
Cristi Vîjdea 6dd8ded05d Update swagger-ui to 3.13.3 2018-04-09 13:55:17 +03:00
Cristi Vîjdea 7270154828 Test with Django Rest Framework 3.8 (#96)
* Add djangorestframework 3.8 to tox and travis
* Add @action tests
* Limit tox to <3.0.0
2018-04-04 22:59:57 +03:00
Cristi Vîjdea 6f7d14fdb2 Add 1.6.1 changelog 2018-04-01 12:08:14 +03:00
shahha 941ec8fdaf Added SUPPORTED_SUBMIT_METHODS swagger ui setting (#91) 2018-03-26 11:15:28 +03:00
Cristi Vîjdea 51ec07261d Improve OAuth2 behaviour and documentation (#90)
* Set OAUTH2_REDIRECT_URL to oauth2-redirect.html by default
* Add example SWAGGER_SETTINGS for OAuth
* Add note about redirect URL
2018-03-24 13:05:37 +02:00
Cristi Vîjdea a9cdf6d561 Fix type of properties in Schema docstring
Fixes #87.
2018-03-21 13:39:14 +02:00
Cristi Vîjdea a64ce95e3c Reset testproj migrations
Heroku deployment issues...
2018-03-18 20:35:02 +02:00
Cristi Vîjdea c9147a5746 Fix testproj people app 2018-03-18 20:20:04 +02:00
Cristi Vîjdea ccdf646c0e Run isort 2018-03-18 20:01:21 +02:00
Cristi Vîjdea 04252ebec3 Remove reodc-alpha artifacts 2018-03-18 19:52:16 +02:00
Cristi Vîjdea 848fb69009 Update swagger-ui to 3.13.0 2018-03-18 19:32:21 +02:00
ko-pp 309a6eb8cd Fix schema generation with OneToOneFields (#81)
* Fix: OneToOneRel, used by OneToOneField doesn't have help_text nor primary_key attributes, thus breaking OpenAPISchemaGenerator; use hasattr() as safe-guard.
* Fix: use getattr() with a default value instead of hasattr() + acessing the value
* Add: 'people' app that breaks drf_yasg without previous commits
* Update tests/references.yaml + run isort and flake8
* Fix: set on_delete for Person.identity as Django-2+ requires it
2018-03-18 19:30:36 +02:00
Cristi Vîjdea a7fbba4967 Update changelog for 1.5.0 2018-03-12 19:26:12 +02:00
Cristi Vîjdea 66785b1ee8 Update swagger-ui to 3.12.1 2018-03-12 19:06:20 +02:00
John Carter fb240f6a5b Detect type of SlugRelatedField with readonly=True (#83)
* Added test for #82 - readonly SlugRelatedField not showing format uuid
* Added ArticleGroup model
* Added example of SlugRelatedField to a uuid field with and without readonly
* Check remote_field.model in get_related_model

Closes #82.
2018-03-10 13:29:20 +02:00
John Carter 2c459024d1 Handle django rest framework HiddenField (#79)
* Added HiddenFieldInspector (#78)
* Updated docs + added test for HiddenField

Added HiddenField to testproj (no change in yaml as expected)

Closes #78.
2018-03-09 22:46:47 +02:00
ko-pp b62161f762 Escape '.' wildcard in testproj urlregex (#80)
Fix: Django's URL patterns being regexes, '.' has to be escaped to be used as a literal value and not as a joker.
2018-03-09 18:28:12 +02:00
Cristi Vîjdea 9ad55bac99 Guard against attempted deletion of inexistent attributes
Fixes #76
2018-03-05 19:59:03 +02:00
Cristi Vîjdea 3d3b7899e5 Update swagger-ui to 3.12.0 and ReDoc to 1.21.2 2018-03-05 17:07:45 +02:00
Cristi Vîjdea 6ea8711a1f Fix in-place modification of swagger_auto_schema arguments (#75)
Fixes #74
2018-03-05 11:51:51 +02:00
Cristi Vîjdea ee46f59fb1 Infer ChoiceField type from model field when in ModelSerializer
Fixes part of issue #69
2018-02-26 20:32:57 +02:00
Santiago Castro 3f7ad62950 Make 1.10.0 the min six version (#71) 2018-02-26 17:18:52 +02:00
Cristi Vîjdea b38d3e6805 Do not set pattern on non-string values
Fixes #68
2018-02-23 18:51:55 +02:00
Cristi Vîjdea 058fd7096d Change README link from pypi.python.org to pypi.org
Description rendering is prettier on the new version.
2018-02-22 20:46:26 +02:00
Cristi Vîjdea a32321d43a Add release dates to changelog 2018-02-22 20:46:22 +02:00
Cristi Vîjdea f15e70b7db Fix missing assignment for default in openapi.Parameter 2018-02-22 20:33:47 +02:00
Cristi Vîjdea 64c280e222 Allow Response objects with no schema
Fixes #66.
2018-02-22 03:46:44 +02:00
Cristi Vîjdea d5073081d9 Fix ModelViewSet Nested bug (#65)
* Add todo repro app
* Add explicit test against model named "Nested"
* Force serializers named NestedSerializer to be output as inline models
* Allow ref_name to rescue a NestedSerializer
* Add tests and documentation
2018-02-22 03:46:16 +02:00
Cristi Vîjdea 10c7e22940 Merge pull request #64 from axnsan12/release/1.4.1
Release version 1.4.1
2018-02-21 05:17:57 +02:00
Cristi Vîjdea 6c497b32b4 Use TemplateHTMLRenderer for error responses in UIRenderer
Closes #58.
2018-02-21 04:59:03 +02:00
Cristi Vîjdea 73c60a8fda Add assertion against mistaken swagger_auto_schema methods usage 2018-02-21 04:42:56 +02:00
Cristi Vîjdea 97082e8898 Update swagger-ui to 3.10.0 and ReDoc to 1.21.0 2018-02-21 04:42:54 +02:00
Cristi Vîjdea 743396617e Upgrade sphinx to 1.7.0 2018-02-21 04:42:53 +02:00
Cristi Vîjdea 97cd1b63d9 Take coerce_to_string into account when handling DecimalField
Closes #62.
2018-02-21 04:42:34 +02:00
Cristi Vîjdea 71dee6eb45 Add OAuth2 client configuration for swagger-ui (#57) 2018-02-04 14:36:54 +02:00
Cristi Vîjdea 7fa0cc0639 Make DRF master test run with stable Django 2018-02-04 14:36:16 +02:00
Cristi Vîjdea e8c688f981 Update swagger-ui to 3.9.3 2018-02-04 14:23:00 +02:00
Cristi Vîjdea c01a6ead26 Bring project into 2018 2018-01-29 00:18:53 +02:00
71 changed files with 2845 additions and 586 deletions
+2
View File
@@ -159,3 +159,5 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
\.pytest_cache/
+5 -3
View File
@@ -8,14 +8,16 @@
<option name="manageScript" value="manage.py" /> <option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" /> <option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" /> <option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/docs/_build" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.6 (drf-yasg)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.6 (drf-yasg)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
@@ -24,7 +26,7 @@
<option name="TEMPLATE_CONFIGURATION" value="Django" /> <option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS"> <option name="TEMPLATE_FOLDERS">
<list> <list>
<option value="$MODULE_DIR$/drf_yasg/templates" /> <option value="$MODULE_DIR$/src/drf_yasg/templates" />
</list> </list>
</option> </option>
</component> </component>
@@ -32,4 +34,4 @@
<option name="projectConfiguration" value="py.test" /> <option name="projectConfiguration" value="py.test" />
<option name="PROJECT_TEST_RUNNER" value="py.test" /> <option name="PROJECT_TEST_RUNNER" value="py.test" />
</component> </component>
</module> </module>
+2 -1
View File
@@ -10,11 +10,12 @@
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions"> <option name="ourVersions">
<value> <value>
<list size="4"> <list size="5">
<item index="0" class="java.lang.String" itemvalue="2.7" /> <item index="0" class="java.lang.String" itemvalue="2.7" />
<item index="1" class="java.lang.String" itemvalue="3.4" /> <item index="1" class="java.lang.String" itemvalue="3.4" />
<item index="2" class="java.lang.String" itemvalue="3.5" /> <item index="2" class="java.lang.String" itemvalue="3.5" />
<item index="3" class="java.lang.String" itemvalue="3.6" /> <item index="3" class="java.lang.String" itemvalue="3.6" />
<item index="4" class="java.lang.String" itemvalue="3.7" />
</list> </list>
</value> </value>
</option> </option>
+3
View File
@@ -70,4 +70,7 @@
</LinkMapSettings> </LinkMapSettings>
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (drf-yasg)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (drf-yasg)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>
</project> </project>
+4 -1
View File
@@ -2,17 +2,20 @@ language: python
cache: pip cache: pip
python: python:
- '2.7'
- '3.4' - '3.4'
- '3.5' - '3.5'
- '3.6' - '3.6'
env: env:
- DRF=3.7 - DRF=3.7
- DRF=3.8
jobs: jobs:
include: include:
- stage: test - stage: test
python: '2.7'
env: DRF=3.7
-
python: '3.6' python: '3.6'
env: DRF=master env: DRF=master
- -
+1 -1
View File
@@ -36,7 +36,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
$ virtualenv venv $ virtualenv venv
$ source venv/bin/activate $ source venv/bin/activate
(venv) $ pip install -e .[validation] (venv) $ pip install -e .[validation]
(venv) $ pip install -rrequirements/dev.txt "Django>=1.11.7" (venv) $ pip install -r requirements/dev.txt
#. **Make your changes and check them against the test project** #. **Make your changes and check them against the test project**
+1 -1
View File
@@ -10,7 +10,7 @@ License
BSD 3-Clause License BSD 3-Clause License
******************** ********************
Copyright (c) 2017, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved. Copyright (c) 2018, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
+9 -3
View File
@@ -11,7 +11,7 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.7.7 - **Django Rest Framework**: 3.7.7, 3.8.x
- **Django**: 1.11.x, 2.0.x - **Django**: 1.11.x, 2.0.x
- **Python**: 2.7, 3.4, 3.5, 3.6 - **Python**: 2.7, 3.4, 3.5, 3.6
@@ -134,7 +134,7 @@ In ``urls.py``:
) )
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
... ...
@@ -353,6 +353,12 @@ Integration with `djangorestframework-camel-case <https://github.com/vbabiy/djan
provided out of the box - if you have ``djangorestframework-camel-case`` installed and your ``APIView`` uses provided out of the box - if you have ``djangorestframework-camel-case`` installed and your ``APIView`` uses
``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer``, all property names will be converted to *camelCase* by default. ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer``, all property names will be converted to *camelCase* by default.
djangorestframework-recursive
===============================
Integration with `djangorestframework-recursive <https://github.com/heywbj/django-rest-framework-recursive>`_ is
provided out of the box - if you have ``djangorestframework-recursive`` installed.
.. |travis| image:: https://img.shields.io/travis/axnsan12/drf-yasg/master.svg .. |travis| image:: https://img.shields.io/travis/axnsan12/drf-yasg/master.svg
:target: https://travis-ci.org/axnsan12/drf-yasg :target: https://travis-ci.org/axnsan12/drf-yasg
:alt: Travis CI :alt: Travis CI
@@ -362,7 +368,7 @@ provided out of the box - if you have ``djangorestframework-camel-case`` install
:alt: Codecov :alt: Codecov
.. |pypi-version| image:: https://img.shields.io/pypi/v/drf-yasg.svg .. |pypi-version| image:: https://img.shields.io/pypi/v/drf-yasg.svg
:target: https://pypi.python.org/pypi/drf-yasg/ :target: https://pypi.org/project/drf-yasg/
:alt: PyPI :alt: PyPI
.. |rtd-badge| image:: https://img.shields.io/readthedocs/drf-yasg.svg .. |rtd-badge| image:: https://img.shields.io/readthedocs/drf-yasg.svg
+225
View File
@@ -2,10 +2,209 @@
Changelog Changelog
######### #########
*********
**1.8.0**
*********
*Release date: Jun 01, 2018*
- **ADDED:** added a :ref:`swagger_schema_fields <swagger_schema_fields>` field on serializer ``Meta`` classes for
customizing schema generation (:issue:`132`, :pr:`134`)
- **FIXED:** error responses from schema views are now rendered with ``JSONRenderer`` instead of throwing
confusing errors (:pr:`130`, :issue:`58`)
- **FIXED:** ``readOnly`` schema fields will now no longer be marked as ``required`` (:pr:`133`)
*********
**1.7.4**
*********
*Release date: May 14, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.14.2
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-alpha.20
- **FIXED:** ignore ``None`` return from ``get_operation`` to avoid empty ``Path`` objects in output
- **FIXED:** request body is now allowed on ``DELETE`` endpoints (:issue:`118`)
*********
**1.7.3**
*********
*Release date: May 12, 2018*
- **FIXED:** views whose ``__init__`` methods throw exceptions will now be ignored during endpoint enumeration
*********
**1.7.2**
*********
*Release date: May 12, 2018*
- **FIXED:** fixed generation of default ``SECURITY_REQUIREMENTS`` to match documented behaviour
- **FIXED:** ordering of ``SECURITY_REQUIREMENTS`` and ``SECURITY_DEFINITIONS`` is now stable
*********
**1.7.1**
*********
*Release date: May 05, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.14.1
- **IMPROVED:** set ``swagger-ui`` ``showCommonExtensions`` to ``True`` by default and add
``SHOW_COMMON_EXTENSIONS`` setting key
- **IMPROVED:** set ``min_length=1`` when ``allow_blank=False`` (:pr:`112`, thanks to :ghuser:`elnappo`)
- **FIXED:** made documentation ordering of ``SwaggerDict`` extra attributes stable
*********
**1.7.0**
*********
*Release date: Apr 27, 2018*
- **ADDED:** added integration with `djangorestframework-recursive <https://github.com/heywbj/django-rest-framework-recursive>`_
(:issue:`109`, :pr:`110`, thanks to :ghuser:`rsichny`)
*NOTE:* in order for this to work, you will have to add the new ``drf_yasg.inspectors.RecursiveFieldInspector`` to
your ``DEFAULT_FIELD_INSPECTORS`` array if you changed it from the default value
- **FIXED:** ``SchemaRef`` now supports cyclical references via the ``ignore_unresolved`` argument
*********
**1.6.2**
*********
*Release date: Apr 25, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.13.6
- **IMPROVED:** switched ``ReDoc`` to version 2.0.0-alpha.17 (was 1.21.2); fixes :issue:`107`
- **FIXED:** made documentation ordering of parameters stable for urls with multiple parameters (:issue:`105`, :pr:`106`)
- **FIXED:** fixed crash when using a model ``ChoiceField`` of unknown child type
*********
**1.6.1**
*********
*Release date: Apr 01, 2018*
- **ADDED:** added ``SUPPORTED_SUBMIT_METHODS`` ``swagger-ui`` setting
*********
**1.6.0**
*********
*Release date: Mar 24, 2018*
- **IMPROVED:** ``OAUTH2_REDIRECT_URL`` will now default to the built in ``oauth2-redirect.html`` file
*********
**1.5.1**
*********
*Release date: Mar 18, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.13.0
- **FIXED:** fixed a crash caused by ``serializers.OneToOneRel`` (:pr:`81`, thanks to :ghuser:`ko-pp`)
*********
**1.5.0**
*********
*Release date: Mar 12, 2018*
- **IMPROVED:** ``serializers.HiddenField`` are now hidden (:issue:`78`, :pr:`79`, thanks to :ghuser:`therefromhere`)
*NOTE:* in order for this to work, you will have to add the new ``drf_yasg.inspectors.HiddenFieldInspector`` to your
``DEFAULT_FIELD_INSPECTORS`` array if you changed it from the default value
- **IMPROVED:** type of model field is now detected for ``serializers.SlugRelatedField`` with ``read_only=True``
(:issue:`82`, :pr:`83`, thanks to :ghuser:`therefromhere`)
*********
**1.4.7**
*********
*Release date: Mar 05, 2018*
- **FIXED:** prevent crashes caused by attempting to delete object attributes which do not exist in the first place
(:issue:`76`)
*********
**1.4.6**
*********
*Release date: Mar 05, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.12.0
- **IMPROVED:** updated ``ReDoc`` to version 1.21.2
*********
**1.4.5**
*********
*Release date: Mar 05, 2018*
- **FIXED:** fixed an issue with modification of ``swagger_auto_schema`` arguments in-place during introspection, which
would sometimes cause an incomplete Swagger document to be generated after the first pass (:issue:`74`, :pr:`75`)
*********
**1.4.4**
*********
*Release date: Feb 26, 2018*
- **IMPROVED:** ``type`` for ``ChoiceField`` generated by a ``ModelSerializer`` from a model field with ``choices=...``
will now be set according to the associated model field (:issue:`69`)
- **FIXED:** ``lookup_field`` and ``lookup_value_regex`` on the same ``ViewSet`` will no longer trigger an exception
(:issue:`68`)
*********
**1.4.3**
*********
*Release date: Feb 22, 2018*
- **FIXED:** added a missing assignment that would cause the ``default`` argument to ``openapi.Parameter.__init__`` to
be ignored
*********
**1.4.2**
*********
*Release date: Feb 22, 2018*
- **FIXED:** fixed a bug that causes a ``ModelViewSet`` generated from models with nested ``ForeignKey`` to output
models named ``Nested`` into the ``definitions`` section (:issue:`59`, :pr:`65`)
- **FIXED:** ``Response`` objects without a ``schema`` are now properly handled when passed through
``swagger_auto_schema`` (:issue:`66`)
*********
**1.4.1**
*********
*Release date: Feb 21, 2018*
- **FIXED:** the ``coerce_to_string`` is now respected when setting the type, default value and min/max values of
``DecimalField`` in the OpenAPI schema (:issue:`62`)
- **FIXED:** error responses from web UI views are now rendered with ``TemplateHTMLRenderer`` instead of throwing
confusing errors (:issue:`58`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.10.0
- **IMPROVED:** updated ``ReDoc`` to version 1.21.0
*********
**1.4.0**
*********
*Release date: Feb 04, 2018*
- **ADDED:** added settings for OAuth2 client configuration in ``swagger-ui`` (:issue:`53`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.9.3
********* *********
**1.3.1** **1.3.1**
********* *********
*Release date: Jan 24, 2018*
- **FIXED:** fixed a bug that would sometimes cause endpoints to wrongly be output as form operations (:issue:`50`) - **FIXED:** fixed a bug that would sometimes cause endpoints to wrongly be output as form operations (:issue:`50`)
- **IMPROVED:** added generation of ``produces`` based on renderer classes - **IMPROVED:** added generation of ``produces`` based on renderer classes
- **IMPROVED:** added generation of top-level ``consumes`` and ``produces`` based on - **IMPROVED:** added generation of top-level ``consumes`` and ``produces`` based on
@@ -15,6 +214,8 @@ Changelog
**1.3.0** **1.3.0**
********* *********
*Release date: Jan 23, 2018*
- **ADDED:** security requirements are now correctly set and can be customized; this should fix problems related - **ADDED:** security requirements are now correctly set and can be customized; this should fix problems related
to authentication in ``swagger-ui`` Try it out! (:issue:`50`, :pr:`54`) to authentication in ``swagger-ui`` Try it out! (:issue:`50`, :pr:`54`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.9.2 - **IMPROVED:** updated ``swagger-ui`` to version 3.9.2
@@ -25,6 +226,8 @@ Changelog
**1.2.2** **1.2.2**
********* *********
*Release date: Jan 12, 2018*
- **FIXED:** djangorestframework>=3.7.7 is now required because of breaking changes - **FIXED:** djangorestframework>=3.7.7 is now required because of breaking changes
(:issue:`44`, :pr:`45`, thanks to :ghuser:`h-hirokawa`) (:issue:`44`, :pr:`45`, thanks to :ghuser:`h-hirokawa`)
@@ -32,12 +235,16 @@ Changelog
**1.2.1** **1.2.1**
********* *********
*Release date: Jan 12, 2018*
- Fixed deployment issues - Fixed deployment issues
********* *********
**1.2.0** **1.2.0**
********* *********
*Release date: Jan 12, 2018 (missing from PyPI due to deployment issues)*
- **ADDED:** ``basePath`` is now generated by taking into account the ``SCRIPT_NAME`` variable and the - **ADDED:** ``basePath`` is now generated by taking into account the ``SCRIPT_NAME`` variable and the
longest common prefix of API urls (:issue:`37`, :pr:`42`) longest common prefix of API urls (:issue:`37`, :pr:`42`)
- **IMPROVED:** removed inline scripts and styles from bundled HTML templates to increase CSP compatibility - **IMPROVED:** removed inline scripts and styles from bundled HTML templates to increase CSP compatibility
@@ -49,6 +256,8 @@ Changelog
**1.1.3** **1.1.3**
********* *********
*Release date: Jan 02, 2018*
- **FIXED:** schema view cache will now always ``Vary`` on the ``Cookie`` and ``Authentication`` (the - **FIXED:** schema view cache will now always ``Vary`` on the ``Cookie`` and ``Authentication`` (the
``Vary`` header was previously only added if ``public`` was set to ``True``) - this fixes issues related to Django ``Vary`` header was previously only added if ``public`` was set to ``True``) - this fixes issues related to Django
authentication in ``swagger-ui`` and ``CurrentUserDefault`` values in the schema authentication in ``swagger-ui`` and ``CurrentUserDefault`` values in the schema
@@ -57,6 +266,8 @@ Changelog
**1.1.2** **1.1.2**
********* *********
*Release date: Jan 01, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.8.1 - **IMPROVED:** updated ``swagger-ui`` to version 3.8.1
- **IMPROVED:** removed some unneeded static files - **IMPROVED:** removed some unneeded static files
@@ -64,6 +275,8 @@ Changelog
**1.1.1** **1.1.1**
********* *********
*Release date: Dec 27, 2017*
- **ADDED:** :ref:`generate_swagger management command <management-command>` - **ADDED:** :ref:`generate_swagger management command <management-command>`
(:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`) (:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`)
- **FIXED:** fixed improper generation of ``\Z`` regex tokens - will now be repalced by ``$`` - **FIXED:** fixed improper generation of ``\Z`` regex tokens - will now be repalced by ``$``
@@ -72,6 +285,8 @@ Changelog
**1.1.0** **1.1.0**
********* *********
*Release date: Dec 27, 2017*
- **ADDED:** added support for APIs versioned with ``URLPathVersioning`` or ``NamespaceVersioning`` - **ADDED:** added support for APIs versioned with ``URLPathVersioning`` or ``NamespaceVersioning``
- **ADDED:** added ability to recursively customize schema generation - **ADDED:** added ability to recursively customize schema generation
:ref:`using pluggable inspector classes <custom-spec-inspectors>` :ref:`using pluggable inspector classes <custom-spec-inspectors>`
@@ -87,6 +302,8 @@ Changelog
**1.0.6** **1.0.6**
********* *********
*Release date: Dec 23, 2017*
- **FIXED:** Swagger UI "Try it out!" should now work with Django login - **FIXED:** Swagger UI "Try it out!" should now work with Django login
- **FIXED:** callable ``default`` values on serializer fields will now be properly called (:pr:`24`, :issue:`25`) - **FIXED:** callable ``default`` values on serializer fields will now be properly called (:pr:`24`, :issue:`25`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.8.0 - **IMPROVED:** updated ``swagger-ui`` to version 3.8.0
@@ -98,6 +315,8 @@ Changelog
**1.0.5** **1.0.5**
********* *********
*Release date: Dec 18, 2017*
- **FIXED:** fixed a crash caused by having read-only Serializers nested by reference - **FIXED:** fixed a crash caused by having read-only Serializers nested by reference
- **FIXED:** removed erroneous backslashes in paths when routes are generated using Django 2 - **FIXED:** removed erroneous backslashes in paths when routes are generated using Django 2
`path() <https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path>`_ `path() <https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path>`_
@@ -109,6 +328,8 @@ Changelog
**1.0.4** **1.0.4**
********* *********
*Release date: Dec 16, 2017*
- **FIXED:** fixed improper generation of YAML references - **FIXED:** fixed improper generation of YAML references
- **ADDED:** added ``query_serializer`` parameter to - **ADDED:** added ``query_serializer`` parameter to
:func:`@swagger_auto_schema <.swagger_auto_schema>` (:issue:`16`, :pr:`17`) :func:`@swagger_auto_schema <.swagger_auto_schema>` (:issue:`16`, :pr:`17`)
@@ -117,6 +338,8 @@ Changelog
**1.0.3** **1.0.3**
********* *********
*Release date: Dec 15, 2017*
- **FIXED:** fixed bug that caused schema views returned from cache to fail (:issue:`14`) - **FIXED:** fixed bug that caused schema views returned from cache to fail (:issue:`14`)
- **FIXED:** disabled automatic generation of response schemas for form operations to avoid confusing errors caused by - **FIXED:** disabled automatic generation of response schemas for form operations to avoid confusing errors caused by
attempting to shove file parameters into Schema objects attempting to shove file parameters into Schema objects
@@ -125,4 +348,6 @@ Changelog
**1.0.2** **1.0.2**
********* *********
*Release date: Dec 13, 2017*
- First published version - First published version
+25 -25
View File
@@ -39,7 +39,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = 'drf-yasg' project = 'drf-yasg'
copyright = '2017, Cristi V.' copyright = '2018, Cristi V.'
author = 'Cristi V.' author = 'Cristi V.'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@@ -162,6 +162,18 @@ autodoc_mock_imports = []
nitpick_ignore = [ nitpick_ignore = [
('py:class', 'object'), ('py:class', 'object'),
('py:class', 'bool'),
('py:class', 'dict'),
('py:class', 'list'),
('py:class', 'str'),
('py:class', 'int'),
('py:class', 'bytes'),
('py:class', 'tuple'),
('py:class', 'callable'),
('py:class', 'type'),
('py:class', 'OrderedDict'),
('py:class', 'None'),
('py:class', 'Exception'), ('py:class', 'Exception'),
('py:class', 'collections.OrderedDict'), ('py:class', 'collections.OrderedDict'),
@@ -174,29 +186,17 @@ nitpick_ignore = [
('py:class', 'OpenAPICodecJson'), ('py:class', 'OpenAPICodecJson'),
('py:class', 'OpenAPISchemaGenerator'), ('py:class', 'OpenAPISchemaGenerator'),
('py:obj', 'bool'), ('py:class', 'coreapi.Field'),
('py:obj', 'dict'), ('py:class', 'BaseFilterBackend'),
('py:obj', 'list'), ('py:class', 'BasePagination'),
('py:obj', 'str'), ('py:class', 'Request'),
('py:obj', 'int'), ('py:class', 'rest_framework.request.Request'),
('py:obj', 'bytes'), ('py:class', 'rest_framework.serializers.Field'),
('py:obj', 'tuple'), ('py:class', 'serializers.Field'),
('py:obj', 'callable'), ('py:class', 'serializers.BaseSerializer'),
('py:obj', 'type'), ('py:class', 'Serializer'),
('py:obj', 'OrderedDict'), ('py:class', 'BaseSerializer'),
('py:obj', 'None'), ('py:class', 'APIView'),
('py:obj', 'coreapi.Field'),
('py:obj', 'BaseFilterBackend'),
('py:obj', 'BasePagination'),
('py:obj', 'Request'),
('py:obj', 'rest_framework.request.Request'),
('py:obj', 'rest_framework.serializers.Field'),
('py:obj', 'serializers.Field'),
('py:obj', 'serializers.BaseSerializer'),
('py:obj', 'Serializer'),
('py:obj', 'BaseSerializer'),
('py:obj', 'APIView'),
] ]
# even though the package should be already installed, the sphinx build on RTD # even though the package should be already installed, the sphinx build on RTD
@@ -273,7 +273,7 @@ def role_github_user(name, rawtext, text, lineno, inliner, options=None, content
options = options or {} options = options or {}
content = content or [] content = content or []
if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", text): if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", text, re.IGNORECASE):
return sphinx_err(inliner, lineno, rawtext, '"%s" is not a valid GitHub username.' % text) return sphinx_err(inliner, lineno, rawtext, '"%s" is not a valid GitHub username.' % text)
ref = gh_user_uri.format(text) ref = gh_user_uri.format(text)
+38 -6
View File
@@ -87,8 +87,8 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have * for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have
to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br| to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br|
Additionally, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based api views, can Additionally, ``@action``\ s, `@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based
respond to multiple HTTP methods and thus have multiple operations that must be decorated separately: api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
.. code-block:: python .. code-block:: python
@@ -96,13 +96,13 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
class ArticleViewSet(viewsets.ModelViewSet): class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the list_route only handles a single method (GET) # method or 'methods' can be skipped because the list_route only handles a single method (GET)
@swagger_auto_schema(operation_description='GET /articles/today/') @swagger_auto_schema(operation_description='GET /articles/today/')
@list_route(methods=['get']) @action(detail=False, methods=['get'])
def today(self, request): def today(self, request):
... ...
@swagger_auto_schema(method='get', operation_description="GET /articles/{id}/image/") @swagger_auto_schema(method='get', operation_description="GET /articles/{id}/image/")
@swagger_auto_schema(method='post', operation_description="POST /articles/{id}/image/") @swagger_auto_schema(method='post', operation_description="POST /articles/{id}/image/")
@detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,)) @action(detail=True, methods=['get', 'post'], parser_classes=(MultiPartParser,))
def image(self, request, id=None): def image(self, request, id=None):
... ...
@@ -169,10 +169,20 @@ You can define some per-serializer options by adding a ``Meta`` class to your se
class Meta: class Meta:
... options here ... ... options here ...
Currently, the only option you can add here is .. _swagger_schema_fields:
The available options are:
* ``ref_name`` - a string which will be used as the model definition name for this serializer class; setting it to * ``ref_name`` - a string which will be used as the model definition name for this serializer class; setting it to
``None`` will force the serializer to be generated as an inline model everywhere it is used ``None`` will force the serializer to be generated as an inline model everywhere it is used. If two serializers
have the same ``ref_name``, both their usages will be replaced with a reference to the same definition.
If this option is not specified, all serializers have an implicit name derived from their class name, minus any
``Serializer`` suffix (e.g. ``UserSerializer`` -> ``User``, ``SerializerWithSuffix`` -> ``SerializerWithSuffix``)
* ``swagger_schema_fields`` - a dictionary mapping :class:`.Schema` field names to values. These attributes
will be set on the :class:`.Schema` object generated from the ``Serializer``. Field names must be python values,
which are converted to Swagger ``Schema`` attribute names according to :func:`.make_swagger_name`.
Attribute names and values must conform to the `OpenAPI 2.0 specification <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject>`_.
************************* *************************
Subclassing and extending Subclassing and extending
@@ -301,3 +311,25 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
This means that you should generally avoid view or method-specific ``FieldInspector``\ s if you are dealing with This means that you should generally avoid view or method-specific ``FieldInspector``\ s if you are dealing with
references (a.k.a named models), because you can never know which view will be the first to generate the schema references (a.k.a named models), because you can never know which view will be the first to generate the schema
for a given serializer. for a given serializer.
**IMPORTANT:** nested fields on ``ModelSerializer``\ s that are generated from model ``ForeignKeys`` will always be
output by value. If you want the by-reference behaviour you have to explictly set the serializer class of nested
fields instead of letting ``ModelSerializer`` generate one automatically; for example:
.. code-block:: python
class OneSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('id',)
class AnotherSerializer(serializers.ModelSerializer):
chilf = OneSerializer()
class Meta:
model = SomeParentModel
fields = ('id', 'child')
Another caveat that stems from this is that any serializer named "``NestedSerializer``" will be forced inline
unless it has a ``ref_name`` set explicitly.
+51 -1
View File
@@ -39,7 +39,7 @@ Security requirements
The second step is specifying, for each endpoint, which authentication mechanism can be used for interacting with it. The second step is specifying, for each endpoint, which authentication mechanism can be used for interacting with it.
See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-requirement-object for details. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-requirement-object for details.
By default, a top-level `security` that accepts all the declared security definitions is generated. By default, a top-level `security` that accepts any one of the declared security definitions is generated.
For the example above, that would be :code:`[{'Basic': []}, {'Bearer': []}]`. This can be overriden using the For the example above, that would be :code:`[{'Basic': []}, {'Bearer': []}]`. This can be overriden using the
:ref:`SECURITY_REQUIREMENTS <security-definitions-settings>` setting. :ref:`SECURITY_REQUIREMENTS <security-definitions-settings>` setting.
@@ -47,3 +47,53 @@ Operation-level overrides can be added using the ``security`` parameter of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`. :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
-------------------------------
``swagger-ui`` as OAuth2 client
-------------------------------
It is possible to configure ``swagger-ui`` to authenticate against your (or a third party) OAuth2 service when sending
"Try it out" requests. This client-side configuration does not remove the requirement of a spec-side
:ref:`security definiiton <security-definitions-settings>`, but merely allows you to test OAuth2 APIs using
``swagger-ui`` as a client.
**DISCLAIMER**: this setup is very poorly tested as I do not currently implement OAuth in any of my projects. All
contributions relating to documentation, bugs, mistakes or anything else are welcome as an issue or pull request. The
settings described below were added as a result of discussion in issue :issue:`53`.
The settings of interest can be found on the :ref:`settings page <oauth2-settings>`. Configuration options are similar
to most OAuth client setups like web or mobile applications. Reading the relevant ``swagger-ui`` docmentation linked
will also probably help.
Example
^^^^^^^
A very simple working configuration was provided by :ghuser:`Vigrond`, originally at
`https://github.com/Vigrond/django_oauth2_example <https://github.com/Vigrond/django_oauth2_example>`_.
.. code-block:: python
SWAGGER_SETTINGS = {
'USE_SESSION_AUTH': False,
'SECURITY_DEFINITIONS': {
'Your App API - Swagger': {
'type': 'oauth2',
'authorizationUrl': '/yourapp/o/authorize',
'tokenUrl': '/yourapp/o/token/',
'flow": "accessCode',
'scopes': {
'read:groups': 'read groups',
}
}
},
'OAUTH2_CONFIG': {
'clientId': 'yourAppClientId',
'clientSecret': 'yourAppClientSecret',
'appName': 'your application name'
},
}
If the OAuth2 provider requires you to provide the full absolute redirect URL, the default value for most
``staticfiles`` configurations will be ``<origin>/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html``. If this is
not suitable for some reason, you can override the ``OAUTH2_REDIRECT_URL`` setting as appropriate.
+50 -8
View File
@@ -66,6 +66,8 @@ to this list.
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SimpleFieldInspector' <.inspectors.SimpleFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.SimpleFieldInspector' <.inspectors.SimpleFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.StringDefaultFieldInspector' <.inspectors.StringDefaultFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.StringDefaultFieldInspector' <.inspectors.StringDefaultFieldInspector>`, |br| \
``]`` ``]``
@@ -189,8 +191,8 @@ OPERATIONS_SORTER
Sorting order for the operation list of each tag. Sorting order for the operation list of each tag.
* :python:`None`: show in the order returned by the server * :python:`None`: show in the order returned by the server
* :python:`alpha`: sort alphabetically by path * :python:`'alpha'`: sort alphabetically by path
* :python:`method`: sort by HTTP method * :python:`'method'`: sort by HTTP method
**Default**: :python:`None` |br| **Default**: :python:`None` |br|
*Maps to parameter*: ``operationsSorter`` *Maps to parameter*: ``operationsSorter``
@@ -201,7 +203,7 @@ TAGS_SORTER
Sorting order for tagged operation groups. Sorting order for tagged operation groups.
* :python:`None`: Swagger UI default ordering * :python:`None`: Swagger UI default ordering
* :python:`alpha`: sort alphabetically * :python:`'alpha'`: sort alphabetically
**Default**: :python:`None` |br| **Default**: :python:`None` |br|
*Maps to parameter*: ``tagsSorter`` *Maps to parameter*: ``tagsSorter``
@@ -211,9 +213,9 @@ DOC_EXPANSION
Controls the default expansion setting for the operations and tags. Controls the default expansion setting for the operations and tags.
* :python:`None`: everything is collapsed * :python:`'none'`: everything is collapsed
* :python:`list`: only tags are expanded * :python:`'list'`: only tags are expanded
* :python:`full`: all operations are expanded * :python:`'full'`: all operations are expanded
**Default**: :python:`'list'` |br| **Default**: :python:`'list'` |br|
*Maps to parameter*: ``docExpansion`` *Maps to parameter*: ``docExpansion``
@@ -239,8 +241,8 @@ DEFAULT_MODEL_RENDERING
Controls whether operations show the model structure or the example value by default. Controls whether operations show the model structure or the example value by default.
* :python:`model`: show the model fields by default * :python:`'model'`: show the model fields by default
* :python:`example`: show the example value by default * :python:`'example'`: show the example value by default
**Default**: :python:`'model'` |br| **Default**: :python:`'model'` |br|
*Maps to parameter*: ``defaultModelRendering`` *Maps to parameter*: ``defaultModelRendering``
@@ -253,6 +255,46 @@ Controls how many levels are expaned by default when showing nested models.
**Default**: :python:`3` |br| **Default**: :python:`3` |br|
*Maps to parameter*: ``defaultModelExpandDepth`` *Maps to parameter*: ``defaultModelExpandDepth``
DEFAULT_MODEL_DEPTH
-------------------
Controls the display of extensions (``pattern``, ``maxLength``, ``minLength``, ``maximum``, ```minimum``) fields and
values for Parameters.
**Default**: :python:`True` |br|
*Maps to parameter*: ``showCommonExtensions``
.. _oauth2-settings:
OAUTH2_REDIRECT_URL
-------------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. If ``None`` is passed, the
``oauth2RedirectUrl`` parameter will be set to ``{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}``. This
is the default `https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html <oauth2-redirect>`_
file provided by ``swagger-ui``.
**Default**: :python:`None` |br|
*Maps to parameter*: ``oauth2RedirectUrl``
OAUTH2_CONFIG
-------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters
to the ``SwaggerUIBundle#initOAuth`` method, and must be a dictionary. See
`OAuth2 configuration <https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md>`_.
**Default**: :python:`{}`
SUPPORTED_SUBMIT_METHODS
------------------------
List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations.
This does not filter the operations from the display.
**Default**: :python:`['get','put','post','delete','options','head','patch','trace']` |br|
*Maps to parameter*: ``supportedSubmitMethods``
****************** ******************
``REDOC_SETTINGS`` ``REDOC_SETTINGS``
****************** ******************
+846 -226
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,7 +1,12 @@
{ {
"name": "drf-yasg", "name": "drf-yasg",
"dependencies": { "dependencies": {
"redoc": "^1.20.0", "redoc": "^2.0.0-alpha.20",
"swagger-ui-dist": "^3.9.2" "swagger-ui-dist": "^3.14.2"
} },
"repository": {
"type": "git",
"url": "https://github.com/axnsan12/drf-yasg.git"
},
"private": true
} }
+2 -3
View File
@@ -4,9 +4,8 @@ openapi_codec>=1.3.2
ruamel.yaml>=0.15.34 ruamel.yaml>=0.15.34
inflection>=0.3.1 inflection>=0.3.1
future>=0.16.0 future>=0.16.0
six>=1.11.0 six>=1.10.0
uritemplate>=3.0.0 uritemplate>=3.0.0
djangorestframework>=3.7.7 djangorestframework>=3.7.7
Django>=1.11.7,<2.0; python_version <= "2.7" Django>=1.11.7
Django>=1.11.7; python_version >= "3.4"
+1 -1
View File
@@ -1,5 +1,5 @@
# used by the 'docs' tox env for building the documentation # used by the 'docs' tox env for building the documentation
Sphinx>=1.6.5 Sphinx>=1.7.0
sphinx_rtd_theme>=0.2.4 sphinx_rtd_theme>=0.2.4
Pillow>=4.3.0 Pillow>=4.3.0
readme_renderer>=17.2 readme_renderer>=17.2
+1 -2
View File
@@ -3,8 +3,7 @@ pytest>=2.9
pytest-pythonpath>=0.7.1 pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1 pytest-cov>=2.5.1
pytest-xdist>=1.22.0 pytest-xdist>=1.22.0
# latest pip version of pytest-django is more than a year old and does not support Django 2.0 pytest-django>=3.2.0
git+https://github.com/pytest-dev/pytest-django.git@94cccb956435dd7a719606744ee7608397e1eafb
datadiff==2.0.0 datadiff==2.0.0
-r testproj.txt -r testproj.txt
+1
View File
@@ -5,5 +5,6 @@ django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7" django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.4" django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=0.2.0 djangorestframework-camel-case>=0.2.0
djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2 dj-database-url>=0.4.2
user_agents>=1.1.0 user_agents>=1.1.0
+1 -1
View File
@@ -1,5 +1,5 @@
# requirements for building and running tox # requirements for building and running tox
tox>=2.9.1 tox>=2.9.1,<3.0.0
detox>=0.11 detox>=0.11
-r setup.txt -r setup.txt
+16 -1
View File
@@ -6,11 +6,13 @@ SWAGGER_DEFAULTS = {
'DEFAULT_FIELD_INSPECTORS': [ 'DEFAULT_FIELD_INSPECTORS': [
'drf_yasg.inspectors.CamelCaseJSONFilter', 'drf_yasg.inspectors.CamelCaseJSONFilter',
'drf_yasg.inspectors.RecursiveFieldInspector',
'drf_yasg.inspectors.ReferencingSerializerInspector', 'drf_yasg.inspectors.ReferencingSerializerInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.ChoiceFieldInspector', 'drf_yasg.inspectors.ChoiceFieldInspector',
'drf_yasg.inspectors.FileFieldInspector', 'drf_yasg.inspectors.FileFieldInspector',
'drf_yasg.inspectors.DictFieldInspector', 'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.HiddenFieldInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.SimpleFieldInspector', 'drf_yasg.inspectors.SimpleFieldInspector',
'drf_yasg.inspectors.StringDefaultFieldInspector', 'drf_yasg.inspectors.StringDefaultFieldInspector',
], ],
@@ -43,6 +45,19 @@ SWAGGER_DEFAULTS = {
'SHOW_EXTENSIONS': True, 'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model', 'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 3, 'DEFAULT_MODEL_DEPTH': 3,
'SHOW_COMMON_EXTENSIONS': True,
'OAUTH2_REDIRECT_URL': None,
'OAUTH2_CONFIG': {},
'SUPPORTED_SUBMIT_METHODS': [
'get',
'put',
'post',
'delete',
'options',
'head',
'patch',
'trace'
],
} }
REDOC_DEFAULTS = { REDOC_DEFAULTS = {
+33 -19
View File
@@ -1,3 +1,4 @@
import copy
import logging import logging
import re import re
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
@@ -85,14 +86,17 @@ class EndpointEnumerator(_EndpointEnumerator):
for pattern in patterns: for pattern in patterns:
path_regex = prefix + get_original_route(pattern) path_regex = prefix + get_original_route(pattern)
if isinstance(pattern, URLPattern): if isinstance(pattern, URLPattern):
path = self.get_path_from_regex(path_regex) try:
callback = pattern.callback path = self.get_path_from_regex(path_regex)
url_name = pattern.name callback = pattern.callback
if self.should_include_endpoint(path, callback, app_name or '', namespace or '', url_name): url_name = pattern.name
path = self.replace_version(path, callback) if self.should_include_endpoint(path, callback, app_name or '', namespace or '', url_name):
for method in self.get_allowed_methods(callback): path = self.replace_version(path, callback)
endpoint = (path, method, callback) for method in self.get_allowed_methods(callback):
api_endpoints.append(endpoint) endpoint = (path, method, callback)
api_endpoints.append(endpoint)
except Exception: # pragma: no cover
logger.warning('failed to enumerate view', exc_info=True)
elif isinstance(pattern, URLResolver): elif isinstance(pattern, URLResolver):
nested_endpoints = self.get_api_endpoints( nested_endpoints = self.get_api_endpoints(
@@ -203,9 +207,15 @@ class OpenAPISchemaGenerator(object):
paths, prefix = self.get_paths(endpoints, components, request, public) paths, prefix = self.get_paths(endpoints, components, request, public)
security_definitions = swagger_settings.SECURITY_DEFINITIONS security_definitions = swagger_settings.SECURITY_DEFINITIONS
if security_definitions is not None:
security_definitions = OrderedDict(sorted([(key, OrderedDict(sorted(sd.items())))
for key, sd in swagger_settings.SECURITY_DEFINITIONS.items()]))
security_requirements = swagger_settings.SECURITY_REQUIREMENTS security_requirements = swagger_settings.SECURITY_REQUIREMENTS
if security_requirements is None: if security_requirements is None:
security_requirements = [{security_scheme: [] for security_scheme in swagger_settings.SECURITY_DEFINITIONS}] security_requirements = [{security_scheme: []} for security_scheme in swagger_settings.SECURITY_DEFINITIONS]
security_requirements = sorted(security_requirements, key=lambda od: list(sorted(od)))
security_requirements = [OrderedDict(sorted(sr.items())) for sr in security_requirements]
url = self.url url = self.url
if url is None and request is not None: if url is None and request is not None:
@@ -314,7 +324,9 @@ class OpenAPISchemaGenerator(object):
if not public and not self._gen.has_view_permissions(path, method, view): if not public and not self._gen.has_view_permissions(path, method, view):
continue continue
operations[method.lower()] = self.get_operation(view, path, prefix, method, components, request) operation = self.get_operation(view, path, prefix, method, components, request)
if operation is not None:
operations[method.lower()] = operation
if operations: if operations:
# since the common prefix is used as the API basePath, it must be stripped # since the common prefix is used as the API basePath, it must be stripped
@@ -355,9 +367,12 @@ class OpenAPISchemaGenerator(object):
view_inspector = view_inspector_cls(view, path, method, components, request, overrides) view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
operation = view_inspector.get_operation(operation_keys) operation = view_inspector.get_operation(operation_keys)
if set(operation.consumes) == set(self.consumes): if operation is None:
return None
if 'consumes' in operation and set(operation.consumes) == set(self.consumes):
del operation.consumes del operation.consumes
if set(operation.produces) == set(self.produces): if 'produces' in operation and set(operation.produces) == set(self.produces):
del operation.produces del operation.produces
return operation return operation
@@ -388,7 +403,7 @@ class OpenAPISchemaGenerator(object):
if method in overrides: if method in overrides:
overrides = overrides[method] overrides = overrides[method]
return overrides return copy.deepcopy(overrides)
def get_path_parameters(self, path, view_cls): def get_path_parameters(self, path, view_cls):
"""Return a list of Parameter instances corresponding to any templated path variables. """Return a list of Parameter instances corresponding to any templated path variables.
@@ -400,17 +415,16 @@ class OpenAPISchemaGenerator(object):
""" """
parameters = [] parameters = []
queryset = getattr(view_cls, 'queryset', None) queryset = getattr(view_cls, 'queryset', None)
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
for variable in 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)
attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING} attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING}
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: if getattr(view_cls, 'lookup_field', None) == variable and attrs['type'] == openapi.TYPE_STRING:
attrs['pattern'] = view_cls.lookup_value_regex attrs['pattern'] = getattr(view_cls, 'lookup_value_regex', attrs.get('pattern', None))
if model_field and model_field.help_text: if model_field and getattr(model_field, 'help_text', False):
description = force_text(model_field.help_text) description = force_text(model_field.help_text)
elif model_field and model_field.primary_key: elif model_field and getattr(model_field, 'primary_key', False):
description = get_pk_description(model, model_field) description = get_pk_description(model, model_field)
else: else:
description = None description = None
+6 -5
View File
@@ -3,8 +3,9 @@ from .base import (
BaseInspector, FieldInspector, FilterInspector, NotHandled, PaginatorInspector, SerializerInspector, ViewInspector BaseInspector, FieldInspector, FilterInspector, NotHandled, PaginatorInspector, SerializerInspector, ViewInspector
) )
from .field import ( from .field import (
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, InlineSerializerInspector, CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector InlineSerializerInspector, RecursiveFieldInspector, ReferencingSerializerInspector, RelatedFieldInspector,
SimpleFieldInspector, StringDefaultFieldInspector
) )
from .query import CoreAPICompatInspector, DjangoRestResponsePagination from .query import CoreAPICompatInspector, DjangoRestResponsePagination
from .view import SwaggerAutoSchema from .view import SwaggerAutoSchema
@@ -22,9 +23,9 @@ __all__ = [
'CoreAPICompatInspector', 'DjangoRestResponsePagination', 'CoreAPICompatInspector', 'DjangoRestResponsePagination',
# field inspectors # field inspectors
'InlineSerializerInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector', 'SimpleFieldInspector', 'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector',
'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'StringDefaultFieldInspector', 'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector',
'CamelCaseJSONFilter', 'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector',
# view inspectors # view inspectors
'SwaggerAutoSchema', 'SwaggerAutoSchema',
+9 -3
View File
@@ -6,7 +6,7 @@ from rest_framework import serializers
from rest_framework.utils import encoders, json from rest_framework.utils import encoders, json
from .. import openapi from .. import openapi
from ..utils import is_list_view from ..utils import decimal_as_float, is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to handle an object #: Sentinel value that inspectors must return to signal that they do not know how to handle an object
NotHandled = object() NotHandled = object()
@@ -224,6 +224,8 @@ class FieldInspector(BaseInspector):
# JSON roundtrip ensures that the value is valid JSON; # JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists # for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder)) default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because " logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True) "to_representation raised an exception", field, exc_info=True)
@@ -232,7 +234,8 @@ class FieldInspector(BaseInspector):
if default is not None: if default is not None:
instance_kwargs['default'] = default instance_kwargs['default'] = default
instance_kwargs.setdefault('title', title) if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY:
instance_kwargs.setdefault('title', title)
instance_kwargs.setdefault('description', description) instance_kwargs.setdefault('description', description)
instance_kwargs.update(kwargs) instance_kwargs.update(kwargs)
@@ -273,7 +276,10 @@ class SerializerInspector(FieldInspector):
class ViewInspector(BaseInspector): class ViewInspector(BaseInspector):
body_methods = ('PUT', 'PATCH', 'POST') #: methods that are allowed to have a request body body_methods = ('PUT', 'PATCH', 'POST', 'DELETE') #: methods that are allowed to have a request body
#: methods that are assumed to require a request body determined by the view's ``serializer_class``
implicit_body_methods = ('PUT', 'PATCH', 'POST')
# real values set in __init__ to prevent import errors # real values set in __init__ to prevent import errors
field_inspectors = [] #: field_inspectors = [] #:
+139 -32
View File
@@ -1,5 +1,7 @@
import logging
import operator import operator
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal
from django.core import validators from django.core import validators
from django.db import models from django.db import models
@@ -8,9 +10,11 @@ from rest_framework.settings import api_settings as rest_framework_settings
from .. import openapi from .. import openapi
from ..errors import SwaggerGenerationError from ..errors import SwaggerGenerationError
from ..utils import filter_none from ..utils import decimal_as_float, filter_none, get_serializer_ref_name
from .base import FieldInspector, NotHandled, SerializerInspector from .base import FieldInspector, NotHandled, SerializerInspector
logger = logging.getLogger(__name__)
class InlineSerializerInspector(SerializerInspector): class InlineSerializerInspector(SerializerInspector):
"""Provides serializer conversions using :meth:`.FieldInspector.field_to_swagger_object`.""" """Provides serializer conversions using :meth:`.FieldInspector.field_to_swagger_object`."""
@@ -18,12 +22,39 @@ class InlineSerializerInspector(SerializerInspector):
#: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section #: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section
use_definitions = False use_definitions = False
def add_manual_fields(self, serializer, schema):
"""Set fields from the ``swagger_schem_fields`` attribute on the serializer's Meta class. This method is called
only for serializers that are converted into ``openapi.Schema`` objects.
:param serializer: serializer instance
:param openapi.Schema schema: the schema object to be modified in-place
"""
serializer_meta = getattr(serializer, 'Meta', None)
swagger_schema_fields = getattr(serializer_meta, 'swagger_schema_fields', {})
if swagger_schema_fields:
for attr, val in swagger_schema_fields.items():
setattr(schema, attr, val)
def get_schema(self, serializer): def get_schema(self, serializer):
return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions) result = self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
schema = openapi.resolve_ref(result, self.components)
self.add_manual_fields(serializer, schema)
return result
def add_manual_parameters(self, serializer, parameters):
"""Add/replace parameters from the given list of automatically generated request parameters. This method
is called only when the serializer is converted into a list of parameters for use in a form data request.
:param serializer: serializer instance
:param list[openapi.Parameter] parameters: genereated parameters
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
return parameters
def get_request_parameters(self, serializer, in_): def get_request_parameters(self, serializer, in_):
fields = getattr(serializer, 'fields', {}) fields = getattr(serializer, 'fields', {})
return [ parameters = [
self.probe_field_inspectors( self.probe_field_inspectors(
value, openapi.Parameter, self.use_definitions, value, openapi.Parameter, self.use_definitions,
name=self.get_parameter_name(key), in_=in_ name=self.get_parameter_name(key), in_=in_
@@ -32,12 +63,17 @@ class InlineSerializerInspector(SerializerInspector):
in fields.items() in fields.items()
] ]
return self.add_manual_parameters(serializer, parameters)
def get_property_name(self, field_name): def get_property_name(self, field_name):
return field_name return field_name
def get_parameter_name(self, field_name): def get_parameter_name(self, field_name):
return field_name return field_name
def get_serializer_ref_name(self, serializer):
return get_serializer_ref_name(serializer)
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
@@ -51,36 +87,37 @@ class InlineSerializerInspector(SerializerInspector):
if swagger_object_type != openapi.Schema: if swagger_object_type != openapi.Schema:
raise SwaggerGenerationError("cannot instantiate nested serializer as " + swagger_object_type.__name__) raise SwaggerGenerationError("cannot instantiate nested serializer as " + swagger_object_type.__name__)
serializer = field ref_name = self.get_serializer_ref_name(field)
serializer_meta = getattr(serializer, 'Meta', None)
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
else:
ref_name = type(serializer).__name__
if ref_name.endswith('Serializer'):
ref_name = ref_name[:-len('Serializer')]
def make_schema_definition(): def make_schema_definition():
properties = OrderedDict() properties = OrderedDict()
required = [] required = []
for property_name, child in serializer.fields.items(): for property_name, child in field.fields.items():
property_name = self.get_property_name(property_name) property_name = self.get_property_name(property_name)
prop_kwargs = { prop_kwargs = {
'read_only': child.read_only or None 'read_only': child.read_only or None
} }
prop_kwargs = filter_none(prop_kwargs) prop_kwargs = filter_none(prop_kwargs)
properties[property_name] = self.probe_field_inspectors( child_schema = self.probe_field_inspectors(
child, ChildSwaggerType, use_references, **prop_kwargs child, ChildSwaggerType, use_references, **prop_kwargs
) )
if child.required: properties[property_name] = child_schema
if child.required and not getattr(child_schema, 'read_only', False):
required.append(property_name) required.append(property_name)
return SwaggerType( result = SwaggerType(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties=properties, properties=properties,
required=required or None, required=required or None,
) )
if not ref_name and 'title' in result:
# on an inline model, the title is derived from the field name
# but is visually displayed like the model name, which is confusing
# it is better to just remove title from inline models
del result.title
return result
if not ref_name or not use_references: if not ref_name or not use_references:
return make_schema_definition() return make_schema_definition()
@@ -147,7 +184,11 @@ def get_related_model(model, source):
:return: related model or ``None`` :return: related model or ``None``
""" """
try: try:
return getattr(model, source).rel.related_model descriptor = getattr(model, source)
try:
return descriptor.rel.related_model
except Exception:
return descriptor.field.remote_field.model
except Exception: # pragma: no cover except Exception: # pragma: no cover
return None return None
@@ -258,18 +299,33 @@ def find_limits(field):
if isinstance(field, field_class) if isinstance(field, field_class)
] ]
if isinstance(field, serializers.DecimalField) and not decimal_as_float(field):
return limits
for validator in field.validators: for validator in field.validators:
if not hasattr(validator, 'limit_value'): if not hasattr(validator, 'limit_value'):
continue continue
limit_value = validator.limit_value
if isinstance(limit_value, Decimal) and decimal_as_float(field):
limit_value = float(limit_value)
for validator_class, attr, improves in applicable_limits: for validator_class, attr, improves in applicable_limits:
if isinstance(validator, validator_class): if isinstance(validator, validator_class):
if attr not in limits or improves(validator.limit_value, limits[attr]): if attr not in limits or improves(limit_value, limits[attr]):
limits[attr] = validator.limit_value limits[attr] = limit_value
if hasattr(field, "allow_blank") and not field.allow_blank:
if limits.get('min_length', 0) < 1:
limits['min_length'] = 1
return OrderedDict(sorted(limits.items())) return OrderedDict(sorted(limits.items()))
def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
model_field_to_basic_type = [ model_field_to_basic_type = [
(models.AutoField, (openapi.TYPE_INTEGER, None)), (models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
@@ -277,7 +333,7 @@ model_field_to_basic_type = [
(models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)), (models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), (models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), (models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(models.DecimalField, (openapi.TYPE_NUMBER, None)), (models.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(models.DurationField, (openapi.TYPE_INTEGER, None)), (models.DurationField, (openapi.TYPE_INTEGER, None)),
(models.FloatField, (openapi.TYPE_NUMBER, None)), (models.FloatField, (openapi.TYPE_NUMBER, None)),
(models.IntegerField, (openapi.TYPE_INTEGER, None)), (models.IntegerField, (openapi.TYPE_INTEGER, None)),
@@ -300,9 +356,11 @@ serializer_field_to_basic_type = [
(serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)), (serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(serializers.RegexField, (openapi.TYPE_STRING, None)), (serializers.RegexField, (openapi.TYPE_STRING, None)),
(serializers.CharField, (openapi.TYPE_STRING, None)), (serializers.CharField, (openapi.TYPE_STRING, None)),
((serializers.BooleanField, serializers.NullBooleanField), (openapi.TYPE_BOOLEAN, None)), (serializers.BooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.IntegerField, (openapi.TYPE_INTEGER, None)), (serializers.IntegerField, (openapi.TYPE_INTEGER, None)),
((serializers.FloatField, serializers.DecimalField), (openapi.TYPE_NUMBER, None)), (serializers.FloatField, (openapi.TYPE_NUMBER, None)),
(serializers.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ? (serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ?
(serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), (serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), (serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
@@ -326,13 +384,18 @@ def get_basic_type_info(field):
for field_class, type_format in basic_type_info: for field_class, type_format in basic_type_info:
if isinstance(field, field_class): if isinstance(field, field_class):
swagger_type, format = type_format swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type(field)
if callable(format): if callable(format):
format = format(field) format = format(field)
break break
else: # pragma: no cover else: # pragma: no cover
return None return None
pattern = find_regex(field) if format in (None, openapi.FORMAT_SLUG) else None pattern = None
if swagger_type == openapi.TYPE_STRING and format in (None, openapi.FORMAT_SLUG):
pattern = find_regex(field)
limits = find_limits(field) limits = find_limits(field)
result = OrderedDict([ result = OrderedDict([
@@ -365,16 +428,29 @@ class ChoiceFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.MultipleChoiceField): if isinstance(field, serializers.ChoiceField):
return SwaggerType( enum_type = openapi.TYPE_STRING
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType( # for ModelSerializer, try to infer the type from the associated model field
type=openapi.TYPE_STRING, serializer = get_parent_serializer(field)
enum=list(field.choices.keys()) if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source)
if model_field:
model_type = get_basic_type_info(model_field)
if model_type:
enum_type = model_type.get('type', enum_type)
if isinstance(field, serializers.MultipleChoiceField):
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType(
type=enum_type,
enum=list(field.choices.keys())
)
) )
)
elif isinstance(field, serializers.ChoiceField): return SwaggerType(type=enum_type, enum=list(field.choices.keys()))
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
return NotHandled return NotHandled
@@ -422,11 +498,21 @@ class DictFieldInspector(FieldInspector):
return NotHandled return NotHandled
class HiddenFieldInspector(FieldInspector):
"""Hide ``HiddenField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if isinstance(field, serializers.HiddenField):
return None
return NotHandled
class StringDefaultFieldInspector(FieldInspector): class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects.""" """For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField HiddenField JSONField # TODO unhandled fields: TimeField JSONField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING) return SwaggerType(type=openapi.TYPE_STRING)
@@ -472,3 +558,24 @@ else:
return camelize_schema(result, self.components) return camelize_schema(result, self.components)
return result return result
try:
from rest_framework_recursive.fields import RecursiveField
except ImportError: # pragma: no cover
class RecursiveFieldInspector(FieldInspector):
"""Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)"""
pass
else:
class RecursiveFieldInspector(FieldInspector):
"""Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)"""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if isinstance(field, RecursiveField) and swagger_object_type == openapi.Schema:
assert use_references is True, "Can not create schema for RecursiveField when use_references is False"
ref_name = get_serializer_ref_name(field.proxied)
assert ref_name is not None, "Can not create RecursiveField schema for inline ModelSerializer"
return openapi.SchemaRef(self.components.with_scope(openapi.SCHEMA_DEFINITIONS), ref_name,
ignore_unresolved=True)
return NotHandled
+6 -3
View File
@@ -101,7 +101,7 @@ class SwaggerAutoSchema(ViewInspector):
if isinstance(body_override, openapi.Schema.OR_REF): if isinstance(body_override, openapi.Schema.OR_REF):
return body_override return body_override
return force_serializer_instance(body_override) return force_serializer_instance(body_override)
elif self.method in self.body_methods: elif self.method in self.implicit_body_methods:
return self.get_view_serializer() return self.get_view_serializer()
return None return None
@@ -144,8 +144,11 @@ class SwaggerAutoSchema(ViewInspector):
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.values()):
raise SwaggerGenerationError("cannot add form parameters when the request has a request schema; " 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:
raise SwaggerGenerationError("form parameters can only be applied to (" + ','.join(self.body_methods) +
") HTTP methods")
parameters.update(param_list_to_odict(manual_parameters)) parameters.update(param_list_to_odict(manual_parameters))
return list(parameters.values()) return list(parameters.values())
@@ -221,7 +224,7 @@ class SwaggerAutoSchema(ViewInspector):
) )
elif isinstance(serializer, openapi.Response): elif isinstance(serializer, openapi.Response):
response = serializer response = serializer
if not isinstance(response.schema, openapi.Schema.OR_REF): if hasattr(response, 'schema') and not isinstance(response.schema, openapi.Schema.OR_REF):
serializer = force_serializer_instance(response.schema) serializer = force_serializer_instance(response.schema)
response.schema = self.serializer_to_schema(serializer) response.schema = self.serializer_to_schema(serializer)
elif isinstance(serializer, openapi.Schema.OR_REF): elif isinstance(serializer, openapi.Schema.OR_REF):
+15 -10
View File
@@ -35,6 +35,7 @@ FORMAT_URI = "uri" #:
# pulled out of my ass # pulled out of my ass
FORMAT_UUID = "uuid" #: FORMAT_UUID = "uuid" #:
FORMAT_SLUG = "slug" #: FORMAT_SLUG = "slug" #:
FORMAT_DECIMAL = "decimal"
IN_BODY = 'body' #: IN_BODY = 'body' #:
IN_PATH = 'path' #: IN_PATH = 'path' #:
@@ -115,7 +116,7 @@ class SwaggerDict(OrderedDict):
which would result in the extra attributes being added first. For this reason, we defer the insertion of the which would result in the extra attributes being added first. For this reason, we defer the insertion of the
attributes and require that subclasses call ._insert_extras__ at the end of their __init__ method. attributes and require that subclasses call ._insert_extras__ at the end of their __init__ method.
""" """
for attr, val in self._extras__.items(): for attr, val in sorted(self._extras__.items()):
setattr(self, attr, val) setattr(self, attr, val)
@staticmethod @staticmethod
@@ -220,7 +221,7 @@ class Swagger(SwaggerDict):
:param str _prefix: api path prefix to use in setting basePath; this will be appended to the wsgi :param str _prefix: api path prefix to use in setting basePath; this will be appended to the wsgi
SCRIPT_NAME prefix or Django's FORCE_SCRIPT_NAME if applicable SCRIPT_NAME prefix or Django's FORCE_SCRIPT_NAME if applicable
:param str _version: version string to override Info :param str _version: version string to override Info
:param list[dict] security_definitions: list of supported authentication mechanisms :param dict[str,dict[str,str]] security_definitions: list of supported authentication mechanisms
:param list[dict] security: authentication mechanisms accepted by default; can be overriden in Operation :param list[dict] security: authentication mechanisms accepted by default; can be overriden in Operation
:param list[str] consumes: consumed MIME types; can be overriden in Operation :param list[str] consumes: consumed MIME types; can be overriden in Operation
:param list[str] produces: produced MIME types; can be overriden in Operation :param list[str] produces: produced MIME types; can be overriden in Operation
@@ -395,6 +396,7 @@ class Parameter(SwaggerDict):
self.enum = enum self.enum = enum
self.pattern = pattern self.pattern = pattern
self.items = items self.items = items
self.default = default
self._insert_extras__() self._insert_extras__()
if self['in'] == IN_PATH: if self['in'] == IN_PATH:
# path parameters must always be required # path parameters must always be required
@@ -423,7 +425,7 @@ class Schema(SwaggerDict):
:param str format: value format, see OpenAPI spec :param str format: value format, see OpenAPI spec
:param list enum: restrict possible values :param list enum: restrict possible values
:param str pattern: pattern if type is ``string`` :param str pattern: pattern if type is ``string``
:param list[.Schema,.SchemaRef] properties: object properties; required if `type` is ``object`` :param dict[str,(.Schema,.SchemaRef)] properties: object properties; required if `type` is ``object``
:param bool,.Schema,.SchemaRef additional_properties: allow wildcard properties not listed in `properties` :param bool,.Schema,.SchemaRef additional_properties: allow wildcard properties not listed in `properties`
:param list[str] required: list of requried property names :param list[str] required: list of requried property names
:param .Schema,.SchemaRef items: type of array items, only valid if `type` is ``array`` :param .Schema,.SchemaRef items: type of array items, only valid if `type` is ``array``
@@ -435,7 +437,7 @@ class Schema(SwaggerDict):
super(Schema, self).__init__(**extra) super(Schema, self).__init__(**extra)
if required is True or required is False: if required is True or required is False:
# common error # common error
raise AssertionError("the `requires` attribute of schema must be an " raise AssertionError("the `required` attribute of schema must be an "
"array of required property names, not a boolean!") "array of required property names, not a boolean!")
assert type, "type is required!" assert type, "type is required!"
self.title = title self.title = title
@@ -464,7 +466,7 @@ class Schema(SwaggerDict):
class _Ref(SwaggerDict): class _Ref(SwaggerDict):
ref_name_re = re.compile(r"#/(?P<scope>.+)/(?P<name>[^/]+)$") ref_name_re = re.compile(r"#/(?P<scope>.+)/(?P<name>[^/]+)$")
def __init__(self, resolver, name, scope, expected_type): def __init__(self, resolver, name, scope, expected_type, ignore_unresolved=False):
"""Base class for all reference types. A reference object has only one property, ``$ref``, which must be a JSON """Base class for all reference types. A reference object has only one property, ``$ref``, which must be a JSON
reference to a valid object in the specification, e.g. ``#/definitions/Article`` to refer to an article model. reference to a valid object in the specification, e.g. ``#/definitions/Article`` to refer to an article model.
@@ -472,13 +474,15 @@ class _Ref(SwaggerDict):
:param str name: referenced object name, e.g. "Article" :param str name: referenced object name, e.g. "Article"
:param str scope: reference scope, e.g. "definitions" :param str scope: reference scope, e.g. "definitions"
:param type[.SwaggerDict] expected_type: the expected type that will be asserted on the object found in resolver :param type[.SwaggerDict] expected_type: the expected type that will be asserted on the object found in resolver
:param bool ignore_unresolved: allow the reference to be not defined in resolver
""" """
super(_Ref, self).__init__() super(_Ref, self).__init__()
assert not type(self) == _Ref, "do not instantiate _Ref directly" assert not type(self) == _Ref, "do not instantiate _Ref directly"
ref_name = "#/{scope}/{name}".format(scope=scope, name=name) ref_name = "#/{scope}/{name}".format(scope=scope, name=name)
obj = resolver.get(name, scope) if not ignore_unresolved:
assert isinstance(obj, expected_type), ref_name + " is a {actual}, not a {expected}" \ obj = resolver.get(name, scope)
.format(actual=type(obj).__name__, expected=expected_type.__name__) assert isinstance(obj, expected_type), ref_name + " is a {actual}, not a {expected}" \
.format(actual=type(obj).__name__, expected=expected_type.__name__)
self.ref = ref_name self.ref = ref_name
def resolve(self, resolver): def resolve(self, resolver):
@@ -500,14 +504,15 @@ class _Ref(SwaggerDict):
class SchemaRef(_Ref): class SchemaRef(_Ref):
def __init__(self, resolver, schema_name): def __init__(self, resolver, schema_name, ignore_unresolved=False):
"""Adds a reference to a named Schema defined in the ``#/definitions/`` object. """Adds a reference to a named Schema defined in the ``#/definitions/`` object.
:param .ReferenceResolver resolver: component resolver which must contain the definition :param .ReferenceResolver resolver: component resolver which must contain the definition
:param str schema_name: schema name :param str schema_name: schema name
:param bool ignore_unresolved: allow the reference to be not defined in resolver
""" """
assert SCHEMA_DEFINITIONS in resolver.scopes assert SCHEMA_DEFINITIONS in resolver.scopes
super(SchemaRef, self).__init__(resolver, schema_name, SCHEMA_DEFINITIONS, Schema) super(SchemaRef, self).__init__(resolver, schema_name, SCHEMA_DEFINITIONS, Schema, ignore_unresolved)
Schema.OR_REF = (Schema, SchemaRef) Schema.OR_REF = (Schema, SchemaRef)
+24 -10
View File
@@ -1,14 +1,16 @@
from django.shortcuts import render, resolve_url from django.shortcuts import render, resolve_url
from rest_framework.renderers import BaseRenderer from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import json from rest_framework.utils import json
from drf_yasg.openapi import Swagger
from .app_settings import redoc_settings, swagger_settings from .app_settings import redoc_settings, swagger_settings
from .codecs import VALIDATORS, OpenAPICodecJson, OpenAPICodecYaml from .codecs import VALIDATORS, OpenAPICodecJson, OpenAPICodecYaml
class _SpecRenderer(BaseRenderer): class _SpecRenderer(BaseRenderer):
"""Base class for text renderers. Handles encoding and validation.""" """Base class for text renderers. Handles encoding and validation."""
charset = None charset = 'utf-8'
validators = [] validators = []
codec_class = None codec_class = None
@@ -20,6 +22,12 @@ class _SpecRenderer(BaseRenderer):
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
assert self.codec_class, "must override codec_class" assert self.codec_class, "must override codec_class"
codec = self.codec_class(self.validators) codec = self.codec_class(self.validators)
if not isinstance(data, Swagger): # pragma: no cover
# if `swagger` is not a ``Swagger`` object, it means we somehow got a non-success ``Response``
# in that case, it's probably better to let the default ``TemplateHTMLRenderer`` render it
# see https://github.com/axnsan12/drf-yasg/issues/58
return JSONRenderer().render(data, media_type, renderer_context)
return codec.encode(data) return codec.encode(data)
@@ -51,6 +59,11 @@ class _UIRenderer(BaseRenderer):
template = '' template = ''
def render(self, swagger, accepted_media_type=None, renderer_context=None): def render(self, swagger, accepted_media_type=None, renderer_context=None):
if not isinstance(swagger, Swagger): # pragma: no cover
# if `swagger` is not a ``Swagger`` object, it means we somehow got a non-success ``Response``
# in that case, it's probably better to let the default ``TemplateHTMLRenderer`` render it
# see https://github.com/axnsan12/drf-yasg/issues/58
return TemplateHTMLRenderer().render(swagger, accepted_media_type, renderer_context)
self.set_context(renderer_context, swagger) self.set_context(renderer_context, swagger)
return render( return render(
renderer_context['request'], renderer_context['request'],
@@ -63,6 +76,7 @@ class _UIRenderer(BaseRenderer):
renderer_context['version'] = swagger.info.version renderer_context['version'] = swagger.info.version
renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings()) renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings())
renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings()) renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings())
renderer_context['oauth2_config'] = json.dumps(self.get_oauth2_config())
renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH
renderer_context.update(self.get_auth_urls()) renderer_context.update(self.get_auth_urls())
@@ -85,6 +99,9 @@ class _UIRenderer(BaseRenderer):
'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING, 'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING,
'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH, 'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
'defaultModelsExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH, 'defaultModelsExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
'showCommonExtensions': swagger_settings.SHOW_COMMON_EXTENSIONS,
'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL,
'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS,
} }
data = {k: v for k, v in data.items() if v is not None} data = {k: v for k, v in data.items() if v is not None}
if swagger_settings.VALIDATOR_URL != '': if swagger_settings.VALIDATOR_URL != '':
@@ -102,6 +119,11 @@ class _UIRenderer(BaseRenderer):
return data return data
def get_oauth2_config(self):
data = swagger_settings.OAUTH2_CONFIG
assert isinstance(data, dict), "OAUTH2_CONFIG must be a dict"
return data
class SwaggerUIRenderer(_UIRenderer): class SwaggerUIRenderer(_UIRenderer):
"""Renders a swagger-ui web interface for schema browisng. """Renders a swagger-ui web interface for schema browisng.
@@ -117,11 +139,3 @@ class ReDocRenderer(_UIRenderer):
""" """
template = 'drf-yasg/redoc.html' template = 'drf-yasg/redoc.html'
format = 'redoc' format = 'redoc'
class ReDocAlphaRenderer(_UIRenderer):
"""Renders a ReDoc web interface for schema browisng.
Also requires :class:`.OpenAPIRenderer` as an available renderer on the same view.
"""
template = 'drf-yasg/redoc-alpha.html'
format = 'redoc'
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -45,11 +45,18 @@
oauth2.auth.code = qp.code; oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else { } else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({ oauth2.errCb({
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "error", level: "error",
message: "Authorization failed: no accessCode received from the server" message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
}); });
} }
} else { } else {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@ function initSwaggerUi() {
], ],
layout: "StandaloneLayout", layout: "StandaloneLayout",
filter: true, filter: true,
requestInterceptor: function(request) { requestInterceptor: function (request) {
var headers = request.headers || {}; var headers = request.headers || {};
var csrftoken = document.querySelector("[name=csrfmiddlewaretoken]"); var csrftoken = document.querySelector("[name=csrfmiddlewaretoken]");
if (csrftoken) { if (csrftoken) {
@@ -49,13 +49,25 @@ function initSwaggerUi() {
}; };
var swaggerSettings = JSON.parse(document.getElementById('swagger-settings').innerHTML); var swaggerSettings = JSON.parse(document.getElementById('swagger-settings').innerHTML);
if (!('oauth2RedirectUrl' in swaggerSettings)) {
var oauth2RedirectUrl = document.getElementById('oauth2-redirect-url');
if (oauth2RedirectUrl) {
swaggerSettings['oauth2RedirectUrl'] = oauth2RedirectUrl.href;
oauth2RedirectUrl.parentNode.removeChild(oauth2RedirectUrl);
}
}
console.log(swaggerSettings);
for (var p in swaggerSettings) { for (var p in swaggerSettings) {
if (swaggerSettings.hasOwnProperty(p)) { if (swaggerSettings.hasOwnProperty(p)) {
swaggerConfig[p] = swaggerSettings[p]; swaggerConfig[p] = swaggerSettings[p];
} }
} }
window.ui = SwaggerUIBundle(swaggerConfig); window.ui = SwaggerUIBundle(swaggerConfig);
var oauth2Config = JSON.parse(document.getElementById('oauth2-config').innerHTML);
console.log(oauth2Config);
window.ui.initOAuth(oauth2Config);
} }
window.onload = function () { window.onload = function () {
@@ -1,18 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/style.css' %}"/>
</head>
<body>
<script id="redoc-settings" type="application/json">{{ redoc_settings | safe }}</script>
<script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
<script src="{% static 'drf-yasg/redoc-init.js' %}"> </script>
<script src="{% static 'drf-yasg/redoc-alpha/redoc.standalone.js' %}"> </script>
</body>
</html>
@@ -60,12 +60,15 @@
<script id="swagger-settings" type="application/json">{{ swagger_settings | safe }}</script> <script id="swagger-settings" type="application/json">{{ swagger_settings | safe }}</script>
<script id="oauth2-config" type="application/json">{{ oauth2_config | safe }}</script>
<script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-bundle.js' %}"></script> <script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
<script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script> <script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
<script src="{% static 'drf-yasg/insQ.min.js' %}"></script> <script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
<script src="{% static 'drf-yasg/swagger-ui-init.js' %}"></script> <script src="{% static 'drf-yasg/swagger-ui-init.js' %}"></script>
<a id="oauth2-redirect-url" href="{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}" class="hidden"></a>
<div id="django-session-auth" class="hidden"> <div id="django-session-auth" class="hidden">
{% if USE_SESSION_AUTH %} {% if USE_SESSION_AUTH %}
{% csrf_token %} {% csrf_token %}
+56 -12
View File
@@ -2,17 +2,24 @@ import inspect
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from django.db import models
from rest_framework import serializers, status from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.request import is_form_media_type from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.views import APIView from rest_framework.views import APIView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
no_body = object()
unset = object() class no_body(object):
"""Used as a sentinel value to forcibly remove the body of a request via :func:`.swagger_auto_schema`."""
pass
class unset(object):
"""Used as a sentinel value for function parameters not set by the caller where ``None`` would be a valid value."""
pass
def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_body=None, query_serializer=None, def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_body=None, query_serializer=None,
@@ -31,7 +38,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
:param .inspectors.SwaggerAutoSchema auto_schema: custom class to use for generating the Operation object; :param .inspectors.SwaggerAutoSchema auto_schema: custom class to use for generating the Operation object;
this overrides both the class-level ``swagger_schema`` attribute and the ``DEFAULT_AUTO_SCHEMA_CLASS`` this overrides both the class-level ``swagger_schema`` attribute and the ``DEFAULT_AUTO_SCHEMA_CLASS``
setting, and can be set to ``None`` to prevent this operation from being generated setting, and can be set to ``None`` to prevent this operation from being generated
:param .Schema,.SchemaRef,.Serializer request_body: custom request body, or :data:`.no_body`. The value given here :param .Schema,.SchemaRef,.Serializer request_body: custom request body, or :class:`.no_body`. The value given here
will be used as the ``schema`` property of a :class:`.Parameter` with ``in: 'body'``. will be used as the ``schema`` property of a :class:`.Parameter` with ``in: 'body'``.
A Schema or SchemaRef is not valid if this request consumes form-data, because ``form`` and ``body`` parameters A Schema or SchemaRef is not valid if this request consumes form-data, because ``form`` and ``body`` parameters
@@ -59,7 +66,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
:param str operation_id: operation ID override; the operation ID must be unique accross the whole API :param str operation_id: operation ID override; the operation ID must be unique accross the whole API
:param str operation_description: operation description override :param str operation_description: operation description override
:param str security: security requirements override; used to specify which authetication mechanism :param list[dict] security: security requirements override; used to specify which authetication mechanism
is requried to call this API; an empty list marks the endpoint as unauthenticated (i.e. removes all accepted is requried to call this API; an empty list marks the endpoint as unauthenticated (i.e. removes all accepted
authentication schemes), and ``None`` will inherit the top-level secuirty requirements authentication schemes), and ``None`` will inherit the top-level secuirty requirements
:param dict[str,(.Schema,.SchemaRef,.Response,str,Serializer)] responses: a dict of documented manual responses :param dict[str,(.Schema,.SchemaRef,.Response,str,Serializer)] responses: a dict of documented manual responses
@@ -107,7 +114,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
# no overrides to set, no use in doing more work # no overrides to set, no use in doing more work
return return
# if the method is a detail_route or list_route, it will have a bind_to_methods attribute # if the method is an @action, it will have a bind_to_methods attribute
bind_to_methods = getattr(view_method, 'bind_to_methods', []) bind_to_methods = getattr(view_method, 'bind_to_methods', [])
# if the method is actually a function based view (@api_view), it will have a 'cls' attribute # if the method is actually a function based view (@api_view), it will have a 'cls' attribute
view_cls = getattr(view_method, 'cls', None) view_cls = getattr(view_method, 'cls', None)
@@ -118,6 +125,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
_methods = methods _methods = methods
if methods or method: if methods or method:
assert available_methods or http_method_names, "`method` or `methods` can only be specified " \
"on @action or @api_view views"
assert bool(methods) != bool(method), "specify either method or methods" assert bool(methods) != bool(method), "specify either method or methods"
assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \ assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \
" use `method` for a single argument" " use `method` for a single argument"
@@ -129,13 +138,13 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
assert not any(mth in existing_data for mth in _methods), "http method defined multiple times" assert not any(mth in existing_data for mth in _methods), "http method defined multiple times"
if available_methods: if available_methods:
# detail_route, list_route or api_view # action or api_view
assert bool(http_method_names) != bool(bind_to_methods), "this should never happen" assert bool(http_method_names) != bool(bind_to_methods), "this should never happen"
if len(available_methods) > 1: if len(available_methods) > 1:
assert _methods, \ assert _methods, \
"on multi-method api_view, detail_route or list_route, you must specify swagger_auto_schema on " \ "on multi-method api_view, action, detail_route or list_route, you must specify " \
"a per-method basis using one of the `method` or `methods` arguments" "swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments"
else: else:
# for a single-method view we assume that single method as the decorator target # for a single-method view we assume that single method as the decorator target
_methods = _methods or available_methods _methods = _methods or available_methods
@@ -147,8 +156,9 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
view_method._swagger_auto_schema = existing_data view_method._swagger_auto_schema = existing_data
else: else:
assert not _methods, \ assert not _methods, \
"the methods argument should only be specified when decorating a detail_route or list_route; you " \ "the methods argument should only be specified when decorating an action, detail_route or " \
"should also ensure that you put the swagger_auto_schema decorator AFTER (above) the _route decorator" "list_route; you should also ensure that you put the swagger_auto_schema decorator " \
"AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method" assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data view_method._swagger_auto_schema = data
@@ -174,7 +184,7 @@ def is_list_view(path, method, view):
return True return True
if action in ('retrieve', 'update', 'partial_update', 'destroy') or detail is True or suffix == 'Instance': if action in ('retrieve', 'update', 'partial_update', 'destroy') or detail is True or suffix == 'Instance':
# a detail_route is surely not a list route # a detail action is surely not a list route
return False return False
# for GenericAPIView, if it's a detail view it can't also be a list view # for GenericAPIView, if it's a detail view it can't also be a list view
@@ -273,3 +283,37 @@ def get_produces(renderer_classes):
media_types = [renderer.media_type for renderer in renderer_classes or []] media_types = [renderer.media_type for renderer in renderer_classes or []]
media_types = [encoding for encoding in media_types if 'html' not in encoding] media_types = [encoding for encoding in media_types if 'html' not in encoding]
return media_types return media_types
def decimal_as_float(field):
"""
Returns true if ``field`` is a django-rest-framework DecimalField and its ``coerce_to_string`` attribute or the
``COERCE_DECIMAL_TO_STRING`` setting is set to ``False``.
:rtype: bool
"""
if isinstance(field, serializers.DecimalField) or isinstance(field, models.DecimalField):
return not getattr(field, 'coerce_to_string', rest_framework_settings.COERCE_DECIMAL_TO_STRING)
return False
def get_serializer_ref_name(serializer):
"""
Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
:param serializer: Serializer instance
:return: Serializer's ref_name or None for inline serializer
:rtype: str or None
"""
serializer_meta = getattr(serializer, 'Meta', None)
serializer_name = type(serializer).__name__
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
logger.debug("Forcing inline output for ModelSerializer named 'NestedSerializer': " + str(serializer))
ref_name = None
else:
ref_name = serializer_name
if ref_name.endswith('Serializer'):
ref_name = ref_name[:-len('Serializer')]
return ref_name
+1 -4
View File
@@ -12,15 +12,12 @@ from rest_framework.views import APIView
from .app_settings import swagger_settings from .app_settings import swagger_settings
from .generators import OpenAPISchemaGenerator from .generators import OpenAPISchemaGenerator
from .renderers import ( from .renderers import OpenAPIRenderer, ReDocRenderer, SwaggerJSONRenderer, SwaggerUIRenderer, SwaggerYAMLRenderer
OpenAPIRenderer, ReDocAlphaRenderer, ReDocRenderer, SwaggerJSONRenderer, SwaggerUIRenderer, SwaggerYAMLRenderer
)
SPEC_RENDERERS = (SwaggerYAMLRenderer, SwaggerJSONRenderer, OpenAPIRenderer) SPEC_RENDERERS = (SwaggerYAMLRenderer, SwaggerJSONRenderer, OpenAPIRenderer)
UI_RENDERERS = { UI_RENDERERS = {
'swagger': (SwaggerUIRenderer, ReDocRenderer), 'swagger': (SwaggerUIRenderer, ReDocRenderer),
'redoc': (ReDocRenderer, SwaggerUIRenderer), 'redoc': (ReDocRenderer, SwaggerUIRenderer),
'redoc-alpha': (ReDocAlphaRenderer, ReDocRenderer, SwaggerUIRenderer)
} }
+22 -1
View File
@@ -1,8 +1,9 @@
# Generated by Django 2.0 on 2017-12-23 09:07 # Generated by Django 2.0.1 on 2018-03-18 18:32
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -23,8 +24,28 @@ class Migration(migrations.Migration):
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)), ('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
('date_created', models.DateTimeField(auto_now_add=True)), ('date_created', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)), ('date_modified', models.DateTimeField(auto_now=True)),
('article_type', models.PositiveSmallIntegerField(choices=[(1, 'first'), (2, 'second'), (3, 'third'), (7, 'seven'), (8, 'eight')], help_text='IntegerField declared on model with choices=(...) and exposed via ModelSerializer', null=True)),
('cover', models.ImageField(blank=True, upload_to='article/original/')), ('cover', models.ImageField(blank=True, upload_to='article/original/')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.CreateModel(
name='ArticleGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)),
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
],
),
migrations.AddField(
model_name='article',
name='group',
field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_main', to='articles.ArticleGroup'),
),
migrations.AddField(
model_name='article',
name='original_group',
field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_original', to='articles.ArticleGroup'),
),
] ]
+17
View File
@@ -1,3 +1,5 @@
import uuid
from django.db import models from django.db import models
@@ -8,5 +10,20 @@ class Article(models.Model):
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True) date_modified = models.DateTimeField(auto_now=True)
author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE) author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE)
article_type = models.PositiveSmallIntegerField(
help_text="IntegerField declared on model with choices=(...) and exposed via ModelSerializer",
choices=((1, "first"), (2, "second"), (3, "third"), (7, "seven"), (8, "eight")), null=True
)
cover = models.ImageField(upload_to='article/original/', blank=True) cover = models.ImageField(upload_to='article/original/', blank=True)
group = models.ForeignKey('ArticleGroup', related_name='articles_as_main', blank=True, default=None,
on_delete=models.PROTECT)
original_group = models.ForeignKey('ArticleGroup', related_name='articles_as_original', blank=True, default=None,
on_delete=models.PROTECT)
class ArticleGroup(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
title = models.CharField(help_text="title model help_text", max_length=255, blank=False, unique=True)
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
+6 -4
View File
@@ -1,7 +1,7 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from articles.models import Article from articles.models import Article, ArticleGroup
class ArticleSerializer(serializers.ModelSerializer): class ArticleSerializer(serializers.ModelSerializer):
@@ -11,12 +11,14 @@ class ArticleSerializer(serializers.ModelSerializer):
read_only=True, read_only=True,
) )
uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True) uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True)
cover_name = serializers.FileField(use_url=False, source='cover', read_only=True) cover_name = serializers.FileField(use_url=False, source='cover', required=True)
group = serializers.SlugRelatedField(slug_field='uuid', queryset=ArticleGroup.objects.all())
original_group = serializers.SlugRelatedField(slug_field='uuid', read_only=True)
class Meta: class Meta:
model = Article model = Article
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
'references', 'uuid', 'cover', 'cover_name') 'references', 'uuid', 'cover', 'cover_name', 'article_type', 'group', 'original_group', )
read_only_fields = ('date_created', 'date_modified', read_only_fields = ('date_created', 'date_modified',
'references', 'uuid', 'cover_name') 'references', 'uuid', 'cover_name')
lookup_field = 'slug' lookup_field = 'slug'
@@ -31,7 +33,7 @@ class ArticleSerializer(serializers.ModelSerializer):
class ImageUploadSerializer(serializers.Serializer): class ImageUploadSerializer(serializers.Serializer):
what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test") what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test", default="69")
image_styles = serializers.ListSerializer( image_styles = serializers.ListSerializer(
child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']), child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']),
help_text="Parameter with Items" help_text="Parameter with Items"
+42 -16
View File
@@ -3,6 +3,7 @@ import datetime
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
@@ -89,23 +90,48 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema swagger_schema = NoTitleAutoSchema
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector]) try:
@list_route(methods=['get']) from rest_framework.decorators import action
def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
articles = self.get_queryset().filter(date_created__range=(today_min, today_max)).all()
serializer = self.serializer_class(articles, many=True)
return Response(serializer.data)
@swagger_auto_schema(method='get', operation_description="image GET description override") @swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@swagger_auto_schema(method='post', request_body=serializers.ImageUploadSerializer) @action(detail=False, methods=['get'])
@detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,)) def today(self, request):
def image(self, request, slug=None): today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
""" today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
image method docstring articles = self.get_queryset().filter(date_created__range=(today_min, today_max)).all()
""" serializer = self.serializer_class(articles, many=True)
pass return Response(serializer.data)
@swagger_auto_schema(method='get', operation_description="image GET description override")
@swagger_auto_schema(method='post', request_body=serializers.ImageUploadSerializer)
@action(detail=True, methods=['get', 'post'], parser_classes=(MultiPartParser,))
def image(self, request, slug=None):
"""
image method docstring
"""
pass
except ImportError:
action = None
# noinspection PyDeprecation
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@list_route(methods=['get'])
def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
articles = self.get_queryset().filter(date_created__range=(today_min, today_max)).all()
serializer = self.serializer_class(articles, many=True)
return Response(serializer.data)
# noinspection PyDeprecation
@swagger_auto_schema(method='get', operation_description="image GET description override")
@swagger_auto_schema(method='post', request_body=serializers.ImageUploadSerializer)
@detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,))
def image(self, request, slug=None):
"""
image method docstring
"""
pass
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
"""update method docstring""" """update method docstring"""
View File
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class PeopleConfig(AppConfig):
name = 'people'
@@ -0,0 +1,30 @@
# Generated by Django 2.0.1 on 2018-03-18 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Identity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstName', models.CharField(max_length=30, null=True)),
('lastName', models.CharField(max_length=30, null=True)),
],
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identity', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='person', to='people.Identity')),
],
),
]
+10
View File
@@ -0,0 +1,10 @@
from django.db import models
class Identity(models.Model):
firstName = models.CharField(max_length=30, null=True)
lastName = models.CharField(max_length=30, null=True)
class Person(models.Model):
identity = models.OneToOneField(Identity, related_name='person', on_delete=models.PROTECT)
+23
View File
@@ -0,0 +1,23 @@
from rest_framework import serializers
from .models import Identity, Person
class IdentitySerializer(serializers.ModelSerializer):
class Meta:
model = Identity
fields = '__all__'
class PersonSerializer(serializers.ModelSerializer):
identity = IdentitySerializer()
class Meta:
model = Person
fields = '__all__'
def create(self, validated_data):
identity = Identity(**validated_data['identity'])
identity.save()
validated_data['identity'] = identity
return super().create(validated_data)
+26
View File
@@ -0,0 +1,26 @@
from django.conf.urls import url
from .views import IdentityViewSet, PersonViewSet
person_list = PersonViewSet.as_view({
'get': 'list',
'post': 'create'
})
person_detail = PersonViewSet.as_view({
'get': 'retrieve',
'patch': 'partial_update',
'delete': 'destroy'
})
identity_detail = IdentityViewSet.as_view({
'get': 'retrieve',
'patch': 'partial_update',
})
urlpatterns = (
url(r'^$', person_list, name='people-list'),
url(r'^(?P<pk>[0-9]+)$', person_detail, name='person-detail'),
url(r'^(?P<person>[0-9]+)/identity$', identity_detail,
name='person-identity'),
)
+16
View File
@@ -0,0 +1,16 @@
from rest_framework import viewsets
from .models import Identity, Person
from .serializers import IdentitySerializer, PersonSerializer
class PersonViewSet(viewsets.ModelViewSet):
model = Person
queryset = Person.objects
serializer_class = PersonSerializer
class IdentityViewSet(viewsets.ModelViewSet):
model = Identity
queryset = Identity.objects
serializer_class = IdentitySerializer
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 2.0 on 2017-12-23 09:07 # Generated by Django 2.0.1 on 2018-03-18 18:32
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
+7 -2
View File
@@ -1,11 +1,13 @@
from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
class LanguageSerializer(serializers.Serializer): class LanguageSerializer(serializers.Serializer):
name = serializers.ChoiceField( name = serializers.ChoiceField(
choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language') choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language')
@@ -14,7 +16,6 @@ class LanguageSerializer(serializers.Serializer):
class ExampleProjectSerializer(serializers.Serializer): class ExampleProjectSerializer(serializers.Serializer):
project_name = serializers.CharField(help_text='Name of the project') project_name = serializers.CharField(help_text='Name of the project')
github_repo = serializers.CharField(required=True, help_text='Github repository of the project') github_repo = serializers.CharField(required=True, help_text='Github repository of the project')
@@ -49,6 +50,10 @@ class SnippetSerializer(serializers.Serializer):
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True) example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True)
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField", difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
read_only=True, default=lambda: 6.9) read_only=True, default=lambda: 6.9)
rate_as_string = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'),
validators=[MinValueValidator(Decimal('0.0'))])
rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'), coerce_to_string=False,
validators=[MinValueValidator(Decimal('0.0'))])
def create(self, validated_data): def create(self, validated_data):
""" """
+36 -9
View File
@@ -1,7 +1,7 @@
from djangorestframework_camel_case.parser import CamelCaseJSONParser from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from inflection import camelize from inflection import camelize
from rest_framework import generics from rest_framework import generics, status
from rest_framework.parsers import FormParser from rest_framework.parsers import FormParser
from drf_yasg import openapi from drf_yasg import openapi
@@ -33,6 +33,21 @@ class SnippetList(generics.ListCreateAPIView):
"""post method docstring""" """post method docstring"""
return super(SnippetList, self).post(request, *args, **kwargs) return super(SnippetList, self).post(request, *args, **kwargs)
@swagger_auto_schema(
operation_id='snippets_delete_bulk',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'body': openapi.Schema(
type=openapi.TYPE_STRING,
description='this should not crash (request body on DELETE method)'
)
}
),
)
def delete(self, *args, **kwargs):
pass
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
@@ -56,14 +71,26 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
"""patch method docstring""" """patch method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs) return super(SnippetDetail, self).patch(request, *args, **kwargs)
@swagger_auto_schema(manual_parameters=[ @swagger_auto_schema(
openapi.Parameter( manual_parameters=[
name='id', in_=openapi.IN_PATH, openapi.Parameter(
type=openapi.TYPE_INTEGER, name='id', in_=openapi.IN_PATH,
description="path parameter override", type=openapi.TYPE_INTEGER,
required=True description="path parameter override",
) required=True
]) ),
openapi.Parameter(
name='delete_form_param', in_=openapi.IN_FORM,
type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)"
),
],
responses={
status.HTTP_204_NO_CONTENT: openapi.Response(
description="this should not crash (response object with no schema)"
)
}
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
"""delete method docstring""" """delete method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs) return super(SnippetDetail, self).patch(request, *args, **kwargs)
+14 -1
View File
@@ -26,6 +26,8 @@ INSTALLED_APPS = [
'snippets', 'snippets',
'users', 'users',
'articles', 'articles',
'todo',
'people'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -92,7 +94,18 @@ SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',
'DEFAULT_INFO': 'testproj.urls.swagger_info' 'DEFAULT_INFO': 'testproj.urls.swagger_info',
'SECURITY_DEFINITIONS': {
'Basic': {
'type': 'basic'
},
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
}
} }
# Internationalization # Internationalization
+3 -1
View File
@@ -51,7 +51,7 @@ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'), url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^redoc/$', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'), url(r'^redoc/$', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
url(r'^redoc-alpha/$', SchemaView.with_ui('redoc-alpha', cache_timeout=0), name='schema-redoc-alpha'),
url(r'^cached/swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=None), name='cschema-json'), url(r'^cached/swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=None), name='cschema-json'),
url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'), url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'),
url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'), url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'),
@@ -62,5 +62,7 @@ urlpatterns = [
url(r'^snippets/', include('snippets.urls')), url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')), url(r'^articles/', include('articles.urls')),
url(r'^users/', include('users.urls')), url(r'^users/', include('users.urls')),
url(r'^todo/', include('todo.urls')),
url(r'^people/', include('people.urls')),
url(r'^plain/', plain_view), url(r'^plain/', plain_view),
] ]
View File
+38
View File
@@ -0,0 +1,38 @@
# Generated by Django 2.0.1 on 2018-03-18 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Todo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TodoAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.Todo')),
],
),
migrations.CreateModel(
name='TodoYetAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.TodoAnother')),
],
),
]
+22
View File
@@ -0,0 +1,22 @@
# Generated by Django 2.0.4 on 2018-04-26 13:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('todo', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TodoTree',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='children', to='todo.TodoTree')),
],
),
]
+20
View File
@@ -0,0 +1,20 @@
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=50)
class TodoAnother(models.Model):
todo = models.ForeignKey(Todo, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
class TodoYetAnother(models.Model):
todo = models.ForeignKey(TodoAnother, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
class TodoTree(models.Model):
parent = models.ForeignKey('self', on_delete=models.CASCADE, related_name='children', null=True)
title = models.CharField(max_length=50)
+58
View File
@@ -0,0 +1,58 @@
from collections import OrderedDict
from django.utils import timezone
from rest_framework import serializers
from rest_framework_recursive.fields import RecursiveField
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('title', 'a_hidden_field',)
a_hidden_field = serializers.HiddenField(default=timezone.now)
class TodoAnotherSerializer(serializers.ModelSerializer):
todo = TodoSerializer()
class Meta:
model = TodoAnother
fields = ('title', 'todo')
class TodoYetAnotherSerializer(serializers.ModelSerializer):
class Meta:
model = TodoYetAnother
fields = ('title', 'todo')
depth = 2
swagger_schema_fields = {
'example': OrderedDict([
('title', 'parent'),
('todo', OrderedDict([
('title', 'child'),
('todo', None),
])),
])
}
class TodoTreeSerializer(serializers.ModelSerializer):
children = serializers.ListField(child=RecursiveField(), source='children.all')
class Meta:
model = TodoTree
fields = ('id', 'title', 'children')
class TodoRecursiveSerializer(serializers.ModelSerializer):
parent = RecursiveField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(queryset=TodoTree.objects.all(), pk_field=serializers.IntegerField(),
write_only=True, allow_null=True, required=False, default=None,
source='parent')
class Meta:
model = TodoTree
fields = ('id', 'title', 'parent', 'parent_id')
+18
View File
@@ -0,0 +1,18 @@
from django.conf.urls import url
from rest_framework import routers
from todo import views
router = routers.DefaultRouter()
router.register(r'', views.TodoViewSet)
router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet)
router.register(r'tree', views.TodoTreeView)
router.register(r'recursive', views.TodoRecursiveView)
urlpatterns = router.urls
urlpatterns += [
url(r'^(?P<todo_id>\d+)/yetanother/(?P<yetanother_id>\d+)/$',
views.NestedTodoView.as_view(), ),
]
+39
View File
@@ -0,0 +1,39 @@
from rest_framework import viewsets
from rest_framework.generics import RetrieveAPIView
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .serializer import (
TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer, TodoYetAnotherSerializer
)
class TodoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
lookup_field = 'id'
lookup_value_regex = '[0-9]+'
class TodoAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoAnother.objects.all()
serializer_class = TodoAnotherSerializer
class TodoYetAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoYetAnother.objects.all()
serializer_class = TodoYetAnotherSerializer
class NestedTodoView(RetrieveAPIView):
serializer_class = TodoYetAnotherSerializer
class TodoTreeView(viewsets.ReadOnlyModelViewSet):
queryset = TodoTree.objects.all()
serializer_class = TodoTreeSerializer
class TodoRecursiveView(viewsets.ModelViewSet):
queryset = TodoTree.objects.all()
serializer_class = TodoRecursiveSerializer
+557 -11
View File
@@ -21,10 +21,15 @@ consumes:
produces: produces:
- application/json - application/json
securityDefinitions: securityDefinitions:
basic: Basic:
type: basic type: basic
Bearer:
in: header
name: Authorization
type: apiKey
security: security:
- basic: [] - Basic: []
- Bearer: []
paths: paths:
/articles/: /articles/:
get: get:
@@ -198,9 +203,11 @@ paths:
- name: what_am_i_doing - name: what_am_i_doing
in: formData in: formData
description: test description: test
required: true required: false
type: string type: string
pattern: ^69$ pattern: ^69$
default: '69'
minLength: 1
- name: image_styles - name: image_styles
in: formData in: formData
description: Parameter with Items description: Parameter with Items
@@ -233,6 +240,113 @@ paths:
type: string type: string
format: slug format: slug
pattern: '[a-z0-9]+(?:-[a-z0-9]+)' pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
/people/:
get:
operationId: people_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/Person'
tags:
- people
post:
operationId: people_create
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Person'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/Person'
tags:
- people
parameters: []
/people/{id}:
get:
operationId: people_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Person'
tags:
- people
patch:
operationId: people_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Person'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Person'
tags:
- people
delete:
operationId: people_delete
description: ''
parameters: []
responses:
'204':
description: ''
tags:
- people
parameters:
- name: id
in: path
description: A unique integer value identifying this person.
required: true
type: integer
/people/{person}/identity:
get:
operationId: people_identity_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Identity'
tags:
- people
patch:
operationId: people_identity_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Identity'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Identity'
tags:
- people
parameters:
- name: person
in: path
required: true
type: string
/plain/: /plain/:
get: get:
operationId: plain_list operationId: plain_list
@@ -274,6 +388,24 @@ paths:
$ref: '#/definitions/Snippet' $ref: '#/definitions/Snippet'
tags: tags:
- snippets - snippets
delete:
operationId: snippetsDeleteBulk
description: SnippetList classdoc
parameters:
- name: data
in: body
required: true
schema:
type: object
properties:
body:
description: this should not crash (request body on DELETE method)
type: string
responses:
'204':
description: ''
tags:
- snippets
parameters: [] parameters: []
/snippets/{id}/: /snippets/{id}/:
get: get:
@@ -328,9 +460,13 @@ paths:
description: path parameter override description: path parameter override
required: true required: true
type: integer type: integer
- name: delete_form_param
in: formData
description: this should not crash (form parameter on DELETE method)
type: integer
responses: responses:
'204': '204':
description: '' description: this should not crash (response object with no schema)
tags: tags:
- snippets - snippets
parameters: parameters:
@@ -339,6 +475,249 @@ paths:
description: A unique integer value identifying this snippet. description: A unique integer value identifying this snippet.
required: true required: true
type: integer type: integer
/todo/:
get:
operationId: todo_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/Todo'
tags:
- todo
parameters: []
/todo/another/:
get:
operationId: todo_another_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/TodoAnother'
tags:
- todo
parameters: []
/todo/another/{id}/:
get:
operationId: todo_another_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoAnother'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this todo another.
required: true
type: integer
/todo/recursive/:
get:
operationId: todo_recursive_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/TodoRecursive'
tags:
- todo
post:
operationId: todo_recursive_create
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/TodoRecursive'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/TodoRecursive'
tags:
- todo
parameters: []
/todo/recursive/{id}/:
get:
operationId: todo_recursive_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoRecursive'
tags:
- todo
put:
operationId: todo_recursive_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/TodoRecursive'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoRecursive'
tags:
- todo
patch:
operationId: todo_recursive_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/TodoRecursive'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoRecursive'
tags:
- todo
delete:
operationId: todo_recursive_delete
description: ''
parameters: []
responses:
'204':
description: ''
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this todo tree.
required: true
type: integer
/todo/tree/:
get:
operationId: todo_tree_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/TodoTree'
tags:
- todo
parameters: []
/todo/tree/{id}/:
get:
operationId: todo_tree_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoTree'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this todo tree.
required: true
type: integer
/todo/yetanother/:
get:
operationId: todo_yetanother_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/TodoYetAnother'
tags:
- todo
parameters: []
/todo/yetanother/{id}/:
get:
operationId: todo_yetanother_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoYetAnother'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this todo yet another.
required: true
type: integer
/todo/{id}/:
get:
operationId: todo_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Todo'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this todo.
required: true
type: integer
/todo/{todo_id}/yetanother/{yetanother_id}/:
get:
operationId: todo_yetanother_read
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/TodoYetAnother'
tags:
- todo
parameters:
- name: todo_id
in: path
required: true
type: string
- name: yetanother_id
in: path
required: true
type: string
/users/: /users/:
get: get:
operationId: users_list operationId: users_list
@@ -349,6 +728,7 @@ paths:
description: this field is generated from a query_serializer description: this field is generated from a query_serializer
required: false required: false
type: string type: string
minLength: 1
- name: is_staff - name: is_staff
in: query in: query
description: this one too! description: this one too!
@@ -442,12 +822,14 @@ definitions:
required: required:
- title - title
- body - body
- group
type: object type: object
properties: properties:
title: title:
description: title model help_text description: title model help_text
type: string type: string
maxLength: 255 maxLength: 255
minLength: 1
author: author:
description: The ID of the user that created this article; if none is provided, description: The ID of the user that created this article; if none is provided,
defaults to the currently logged in user. defaults to the currently logged in user.
@@ -457,6 +839,7 @@ definitions:
description: body serializer help_text description: body serializer help_text
type: string type: string
maxLength: 5000 maxLength: 5000
minLength: 1
slug: slug:
description: slug model help_text description: slug model help_text
type: string type: string
@@ -478,6 +861,7 @@ definitions:
description: but i needed to test these 2 fields somehow description: but i needed to test these 2 fields somehow
type: string type: string
format: uri format: uri
minLength: 1
readOnly: true readOnly: true
uuid: uuid:
description: should articles have UUIDs? description: should articles have UUIDs?
@@ -491,6 +875,52 @@ definitions:
cover_name: cover_name:
type: string type: string
readOnly: true readOnly: true
article_type:
description: IntegerField declared on model with choices=(...) and exposed
via ModelSerializer
type: integer
enum:
- 1
- 2
- 3
- 7
- 8
group:
type: string
format: uuid
original_group:
type: string
format: uuid
readOnly: true
Identity:
title: Identity
type: object
properties:
id:
title: ID
type: integer
readOnly: true
firstName:
title: FirstName
type: string
maxLength: 30
minLength: 1
lastName:
title: LastName
type: string
maxLength: 30
minLength: 1
Person:
required:
- identity
type: object
properties:
id:
title: ID
type: integer
readOnly: true
identity:
$ref: '#/definitions/Identity'
Project: Project:
required: required:
- projectName - projectName
@@ -501,10 +931,12 @@ definitions:
title: Project name title: Project name
description: Name of the project description: Name of the project
type: string type: string
minLength: 1
githubRepo: githubRepo:
title: Github repo title: Github repo
description: Github repository of the project description: Github repository of the project
type: string type: string
minLength: 1
Snippet: Snippet:
required: required:
- code - code
@@ -526,6 +958,7 @@ definitions:
description: The ID of the user that created this snippet. description: The ID of the user that created this snippet.
type: string type: string
readOnly: true readOnly: true
minLength: 1
title: Owner as string title: Owner as string
title: title:
title: Title title: Title
@@ -534,11 +967,11 @@ definitions:
code: code:
title: Code title: Code
type: string type: string
minLength: 1
linenos: linenos:
title: Linenos title: Linenos
type: boolean type: boolean
language: language:
title: Language
description: Sample help text for language description: Sample help text for language
type: object type: object
properties: properties:
@@ -983,7 +1416,6 @@ definitions:
- zephir - zephir
default: python default: python
styles: styles:
title: Styles
type: array type: array
items: items:
type: string type: string
@@ -1020,12 +1452,10 @@ definitions:
default: default:
- friendly - friendly
lines: lines:
title: Lines
type: array type: array
items: items:
type: integer type: integer
exampleProjects: exampleProjects:
title: Example projects
type: array type: array
items: items:
$ref: '#/definitions/Project' $ref: '#/definitions/Project'
@@ -1036,6 +1466,123 @@ definitions:
type: number type: number
readOnly: true readOnly: true
default: 6.9 default: 6.9
rateAsString:
title: Rate as string
type: string
format: decimal
default: '0.000'
rate:
title: Rate
type: number
format: decimal
default: 0.0
minimum: 0.0
Todo:
required:
- title
type: object
properties:
title:
title: Title
type: string
maxLength: 50
minLength: 1
TodoAnother:
required:
- title
- todo
type: object
properties:
title:
title: Title
type: string
maxLength: 50
minLength: 1
todo:
$ref: '#/definitions/Todo'
TodoRecursive:
required:
- title
type: object
properties:
id:
title: ID
type: integer
readOnly: true
title:
title: Title
type: string
maxLength: 50
minLength: 1
parent:
$ref: '#/definitions/TodoRecursive'
parent_id:
type: integer
title: Parent id
TodoTree:
required:
- title
- children
type: object
properties:
id:
title: ID
type: integer
readOnly: true
title:
title: Title
type: string
maxLength: 50
minLength: 1
children:
type: array
items:
$ref: '#/definitions/TodoTree'
TodoYetAnother:
required:
- title
type: object
properties:
title:
title: Title
type: string
maxLength: 50
minLength: 1
todo:
required:
- title
type: object
properties:
id:
title: ID
type: integer
readOnly: true
title:
title: Title
type: string
maxLength: 50
minLength: 1
todo:
required:
- title
type: object
properties:
id:
title: ID
type: integer
readOnly: true
title:
title: Title
type: string
maxLength: 50
minLength: 1
readOnly: true
readOnly: true
example:
title: parent
todo:
title: child
todo: null
UserSerializerrr: UserSerializerrr:
required: required:
- username - username
@@ -1054,19 +1601,18 @@ definitions:
type: string type: string
pattern: ^[\w.@+-]+$ pattern: ^[\w.@+-]+$
maxLength: 150 maxLength: 150
minLength: 1
email: email:
title: Email address title: Email address
type: string type: string
format: email format: email
maxLength: 254 maxLength: 254
articles: articles:
title: Articles
type: array type: array
items: items:
type: integer type: integer
uniqueItems: true uniqueItems: true
snippets: snippets:
title: Snippets
type: array type: array
items: items:
type: integer type: integer
@@ -1077,6 +1623,7 @@ definitions:
type: string type: string
format: ipv4 format: ipv4
readOnly: true readOnly: true
minLength: 1
last_connected_at: last_connected_at:
title: Last connected at title: Last connected at
description: really? description: really?
@@ -1084,7 +1631,6 @@ definitions:
format: date format: date
readOnly: true readOnly: true
article_slugs: article_slugs:
title: Article slugs
type: array type: array
items: items:
type: string type: string
+6
View File
@@ -46,3 +46,9 @@ def test_noop_inspectors(swagger_settings, mock_schema_request, codec_json, refe
json_bytes = codec_json.encode(swagger) json_bytes = codec_json.encode(swagger)
swagger_dict = json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict) swagger_dict = json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict)
compare_schemas(swagger_dict, reference_schema) compare_schemas(swagger_dict, reference_schema)
def test_no_nested_model(swagger_dict):
# ForeignKey models in deep ModelViewSets might wrongly be labeled as 'Nested' in the definitions section
# see https://github.com/axnsan12/drf-yasg/issues/59
assert 'Nested' not in swagger_dict['definitions']
+34
View File
@@ -2,11 +2,14 @@ import json
from collections import OrderedDict from collections import OrderedDict
import pytest import pytest
from rest_framework import routers, serializers, viewsets
from rest_framework.response import Response
from drf_yasg import codecs, openapi from drf_yasg import codecs, openapi
from drf_yasg.codecs import yaml_sane_load from drf_yasg.codecs import yaml_sane_load
from drf_yasg.errors import SwaggerGenerationError from drf_yasg.errors import SwaggerGenerationError
from drf_yasg.generators import OpenAPISchemaGenerator from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema
def test_schema_is_valid(swagger, codec_yaml): def test_schema_is_valid(swagger, codec_yaml):
@@ -79,3 +82,34 @@ def test_securiy_requirements(swagger_settings, mock_schema_request):
swagger = generator.get_schema(mock_schema_request, public=True) swagger = generator.get_schema(mock_schema_request, public=True)
assert swagger['security'] == [] assert swagger['security'] == []
def test_replaced_serializer():
class DetailSerializer(serializers.Serializer):
detail = serializers.CharField()
class DetailViewSet(viewsets.ViewSet):
serializer_class = DetailSerializer
@swagger_auto_schema(responses={404: openapi.Response("Not found or Not accessible", DetailSerializer)})
def retrieve(self, request, pk=None):
serializer = DetailSerializer({'detail': None})
return Response(serializer.data)
router = routers.DefaultRouter()
router.register(r'details', DetailViewSet, base_name='details')
generator = OpenAPISchemaGenerator(
info=openapi.Info(title="Test generator", default_version="v1"),
version="v2",
url='',
patterns=router.urls
)
for _ in range(3):
swagger = generator.get_schema(None, True)
assert 'Detail' in swagger['definitions']
assert 'detail' in swagger['definitions']['Detail']['properties']
responses = swagger['paths']['/details/{id}/']['get']['responses']
assert '404' in responses
assert responses['404']['schema']['$ref'] == "#/definitions/Detail"
+15
View File
@@ -1,3 +1,6 @@
from collections import OrderedDict
from random import shuffle
from drf_yasg import openapi from drf_yasg import openapi
@@ -51,3 +54,15 @@ def test_trailing_underscore_stripped():
del sd.in_ del sd.in_
assert 'in' not in sd assert 'in' not in sd
assert not hasattr(sd, 'in__') assert not hasattr(sd, 'in__')
def test_extra_ordering():
"""Insertion order should also be consistent when setting undeclared parameters (kwargs) in SwaggerDict"""
extras = [('beta', 1), ('alpha', 2), ('omega', 3), ('gamma', 4)]
shuffled_extras = list(extras)
shuffle(shuffled_extras)
s1 = openapi.SwaggerDict(**OrderedDict(extras))
s2 = openapi.SwaggerDict(**OrderedDict(shuffled_extras))
assert list(s1.items()) == list(s2.items())
+10 -12
View File
@@ -1,13 +1,14 @@
[tox] [tox]
envlist = envlist =
py27-django111-drf37, py27-django111-drf37,
py{34,35,36}-django{111,20}-drf37, py{34,35,36}-django{111,20}-drf{37,38},
py36-drfmaster, py36-django20-drfmaster,
lint, docs lint, docs
[travis:env] [travis:env]
DRF = DRF =
3.7: drf37 3.7: drf37
3.8: drf38
master: drfmaster master: drfmaster
[testenv] [testenv]
@@ -16,10 +17,10 @@ deps =
django20: Django>=2.0,<2.1 django20: Django>=2.0,<2.1
drf37: djangorestframework>=3.7.7,<3.8 drf37: djangorestframework>=3.7.7,<3.8
drf38: djangorestframework>=3.8.0,<3.9
# test with the latest build of Django and django-rest-framework to get early warning of compatibility issues # test with the latest build of django-rest-framework to get early warning of compatibility issues
drfmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz drfmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
drfmaster: https://github.com/django/django/archive/master.tar.gz
# other dependencies # other dependencies
-rrequirements/validation.txt -rrequirements/validation.txt
@@ -28,9 +29,6 @@ deps =
commands = commands =
pytest --cov --cov-config .coveragerc --cov-append --cov-report="" {posargs} pytest --cov --cov-config .coveragerc --cov-append --cov-report="" {posargs}
[testenv:py36-drfmaster]
pip_pre = True
[testenv:lint] [testenv:lint]
skip_install = true skip_install = true
deps = deps =
@@ -48,7 +46,7 @@ commands =
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = testproj.settings.local DJANGO_SETTINGS_MODULE = testproj.settings.local
python_paths = testproj python_paths = testproj
addopts = -n 3 addopts = -n 3 --ignore=node_modules
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
@@ -67,7 +65,7 @@ known_standard_library =
collections,copy,distutils,functools,inspect,io,json,logging,operator,os,pkg_resources,re,setuptools,sys, collections,copy,distutils,functools,inspect,io,json,logging,operator,os,pkg_resources,re,setuptools,sys,
types,warnings types,warnings
known_third_party = known_third_party =
coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,flex,gunicorn, coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,
inflection,pygments,pytest,rest_framework,ruamel,setuptools_scm,swagger_spec_validator,uritemplate,user_agents, rest_framework_recursive,flex,gunicorn,inflection,pygments,pytest,rest_framework,ruamel,setuptools_scm,
whitenoise swagger_spec_validator,uritemplate,user_agents,whitenoise
known_first_party = drf_yasg,testproj,articles,snippets,users,urlconfs known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs
+1 -1
View File
@@ -2,7 +2,7 @@
set -ev set -ev
npm update npm update
cp node_modules/redoc/dist/redoc.min.js src/drf_yasg/static/drf-yasg/redoc/ cp node_modules/redoc/bundles/redoc.standalone.js src/drf_yasg/static/drf-yasg/redoc/redoc.min.js
cp -r node_modules/swagger-ui-dist src/drf_yasg/static/drf-yasg/ cp -r node_modules/swagger-ui-dist src/drf_yasg/static/drf-yasg/
pushd src/drf_yasg/static/drf-yasg/swagger-ui-dist/ >/dev/null pushd src/drf_yasg/static/drf-yasg/swagger-ui-dist/ >/dev/null
rm -f package.json .npmignore README.md rm -f package.json .npmignore README.md