├── .codecov.yml ├── .coveragerc ├── .cspell.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE │ └── general.md ├── actions │ └── install │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── publish.yml │ └── review.yml ├── .gitignore ├── .readthedocs.yml ├── CONTRIBUTING.rst ├── LICENSE.rst ├── MANIFEST.in ├── Procfile ├── README.rst ├── app.json ├── docs ├── Makefile ├── _static │ ├── .gitignore │ └── css │ │ └── style.css ├── _templates │ ├── .gitignore │ └── layout.html ├── changelog.rst ├── conf.py ├── contributing.rst ├── custom_spec.rst ├── custom_ui.rst ├── drf_yasg.rst ├── images │ └── flow.png ├── index.rst ├── license.rst ├── make.bat ├── openapi.rst ├── readme.rst ├── rendering.rst ├── security.rst └── settings.rst ├── package-lock.json ├── package.json ├── pyproject.toml ├── requirements.txt ├── requirements ├── base.txt ├── ci.txt ├── dev.txt ├── docs.txt ├── heroku.txt ├── lint.txt ├── publish.txt ├── test.txt ├── testproj.txt ├── tox.txt └── validation.txt ├── runtime.txt ├── screenshots ├── redoc-nested-response.png ├── swagger-ui-list.png └── swagger-ui-models.png ├── scripts └── linters │ └── labels.js ├── setup.cfg ├── setup.py ├── src └── drf_yasg │ ├── __init__.py │ ├── app_settings.py │ ├── codecs.py │ ├── errors.py │ ├── generators.py │ ├── inspectors │ ├── __init__.py │ ├── base.py │ ├── field.py │ ├── query.py │ └── view.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_swagger.py │ ├── middleware.py │ ├── openapi.py │ ├── renderers.py │ ├── static │ └── drf-yasg │ │ ├── README │ │ ├── immutable.js │ │ ├── immutable.min.js │ │ ├── insQ.js │ │ ├── insQ.min.js │ │ ├── redoc-init.js │ │ ├── redoc-old │ │ ├── LICENSE │ │ ├── redoc.min.js │ │ └── redoc.min.js.map │ │ ├── redoc │ │ ├── LICENSE │ │ ├── redoc-logo.png │ │ ├── redoc.min.js │ │ └── redoc.standalone.js.map │ │ ├── style.css │ │ ├── swagger-ui-dist │ │ ├── LICENSE │ │ ├── NOTICE │ │ ├── absolute-path.js │ │ ├── favicon-32x32.png │ │ ├── index.css │ │ ├── index.js │ │ ├── oauth2-redirect.html │ │ ├── swagger-initializer.js │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-es-bundle-core.js │ │ ├── swagger-ui-es-bundle-core.js.map │ │ ├── swagger-ui-es-bundle.js │ │ ├── swagger-ui-es-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.css │ │ ├── swagger-ui.css.map │ │ └── swagger-ui.js.map │ │ └── swagger-ui-init.js │ ├── templates │ └── drf-yasg │ │ ├── redoc-old.html │ │ ├── redoc.html │ │ └── swagger-ui.html │ ├── utils.py │ └── views.py ├── testproj ├── articles │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_article_read_only_nullable.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── manage.py ├── people │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_rename_identity_fields.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── requirements.txt ├── snippets │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181219_1016.py │ │ ├── 0003_snippetviewer.py │ │ ├── 0004_auto_20190613_0154.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── testproj │ ├── __init__.py │ ├── inspectors.py │ ├── runner.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── heroku.py │ │ └── local.py │ ├── static │ │ └── .gitkeep │ ├── templates │ │ └── .gitkeep │ ├── urls.py │ ├── util.py │ └── wsgi.py ├── todo │ ├── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_todotree.py │ │ ├── 0003_pack.py │ │ └── __init__.py │ ├── models.py │ ├── serializer.py │ ├── urls.py │ └── views.py └── users │ ├── __init__.py │ ├── method_serializers.py │ ├── migrations │ ├── 0001_create_admin_user.py │ ├── 0002_setup_oauth2_apps.py │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── tests ├── conftest.py ├── reference.yaml ├── test_form_parameters.py ├── test_get_basic_type_info_from_hint.py ├── test_management.py ├── test_reference_schema.py ├── test_referenceresolver.py ├── test_renderer_settings.py ├── test_schema_generator.py ├── test_schema_views.py ├── test_swaggerdict.py ├── test_versioning.py └── urlconfs │ ├── __init__.py │ ├── additional_fields_checks.py │ ├── coreschema.py │ ├── legacy_renderer.py │ ├── login_test_urls.py │ ├── non_public_urls.py │ ├── ns_version1.py │ ├── ns_version2.py │ ├── ns_versioning.py │ ├── overrided_serializer_name.py │ ├── url_versioning.py │ └── url_versioning_extra.py ├── tox.ini └── update-ui.sh /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: 60...100 8 | 9 | status: 10 | project: 11 | default: 12 | enabled: yes 13 | target: auto 14 | threshold: 100% 15 | if_no_uploads: error 16 | if_ci_failed: error 17 | 18 | patch: 19 | default: 20 | enabled: yes 21 | target: 100% 22 | threshold: 100% 23 | if_no_uploads: error 24 | if_ci_failed: error 25 | 26 | changes: 27 | default: 28 | enabled: no 29 | 30 | comment: false 31 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = drf_yasg 3 | branch = True 4 | parallel = true 5 | disable_warnings = module-not-measured 6 | 7 | [report] 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | # Have to re-enable the standard pragma 11 | pragma: no cover 12 | 13 | # Don't complain about missing debug-only code: 14 | def __repr__ 15 | if self/.debug 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise ImproperlyConfigured 20 | raise TypeError 21 | raise NotImplementedError 22 | warnings.warn 23 | logger.debug 24 | logger.info 25 | logger.warning 26 | logger.error 27 | return NotHandled 28 | 29 | # Don't complain if non-runnable code isn't run: 30 | if 0: 31 | if __name__ == .__main__.: 32 | 33 | # Don't complain if we don't hit invalid schema configurations 34 | raise SwaggerGenerationError 35 | 36 | ignore_errors = True 37 | precision = 2 38 | show_missing = True 39 | 40 | [paths] 41 | source = 42 | src/drf_yasg/ 43 | .tox/*/Lib/site-packages/drf_yasg/ 44 | .tox/*/lib/*/site-packages/drf_yasg/ 45 | /home/travis/virtualenv/*/lib/*/site-packages/drf_yasg/ 46 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "ignorePaths": [ 4 | "./app.json", 5 | ".gitignore", 6 | "*.coverage", 7 | "*.min.js", 8 | "**/__pycache__/**", 9 | "**/*.egg-info/**", 10 | "**/*.git/**", 11 | "**/build/**", 12 | "**/coverage/**", 13 | "**/dist/**", 14 | "**/migrations/**", 15 | "**/swagger-ui-dist/**", 16 | "**/venv/**" 17 | ], 18 | "dictionaries": [ 19 | "css", 20 | "django", 21 | "fonts", 22 | "local", 23 | "misc", 24 | "python", 25 | "softwareTerms" 26 | ], 27 | "words": [ 28 | "addopts", 29 | "apiview", 30 | "askar", 31 | "auths", 32 | "authtoken", 33 | "autoclass", 34 | "autodata", 35 | "automodule", 36 | "avenir", 37 | "barebones", 38 | "basepath", 39 | "beaugunderson", 40 | "blueyed", 41 | "builddir", 42 | "bysource", 43 | "cacheable", 44 | "callabale", 45 | "camelize", 46 | "camelized", 47 | "classdoc", 48 | "codecov", 49 | "codegen", 50 | "coreapi", 51 | "coreschema", 52 | "corsheaders", 53 | "coveragerc", 54 | "cristi", 55 | "cristian", 56 | "cschema", 57 | "csrfmiddlewaretoken", 58 | "csrftoken", 59 | "dascalescu", 60 | "datadiff", 61 | "deauth", 62 | "deauthorize", 63 | "deprecated", 64 | "djangorestframework", 65 | "djmaster", 66 | "docstrings", 67 | "documentclass", 68 | "elnappo", 69 | "envlist", 70 | "eryk", 71 | "exitfirst", 72 | "extrahead", 73 | "figwidth", 74 | "filterset", 75 | "formop", 76 | "genindex", 77 | "getdefault", 78 | "ghuser", 79 | "gunicorn", 80 | "herokuapp", 81 | "hirokawa", 82 | "howto", 83 | "htbp", 84 | "htmlhelp", 85 | "immutablehash", 86 | "indentless", 87 | "initkwargs", 88 | "joellefkowitz", 89 | "jsons", 90 | "keepdb", 91 | "keyframeprefix", 92 | "letterpaper", 93 | "levelname", 94 | "linenos", 95 | "maxdepth", 96 | "minversion", 97 | "modindex", 98 | "monokai", 99 | "myparent", 100 | "napierała", 101 | "nbsp", 102 | "noscm", 103 | "npmignore", 104 | "odict", 105 | "omap", 106 | "paginators", 107 | "papersize", 108 | "passwordadmin", 109 | "plugable", 110 | "pointsize", 111 | "popd", 112 | "posargs", 113 | "preauth", 114 | "preauthorize", 115 | "prepended", 116 | "proxied", 117 | "psycopg", 118 | "pushd", 119 | "putenv", 120 | "pythonpath", 121 | "pytz", 122 | "qinsq", 123 | "quickstart", 124 | "rebilly", 125 | "redoc", 126 | "referenceable", 127 | "reftest", 128 | "refuri", 129 | "regexes", 130 | "representer", 131 | "rsichny", 132 | "rtype", 133 | "ruamel", 134 | "scrollbars", 135 | "searchbox", 136 | "serializers", 137 | "setuptools", 138 | "sidemenu", 139 | "sourcedir", 140 | "sphinxbuild", 141 | "sphinxopts", 142 | "sphinxproj", 143 | "staticfiles", 144 | "subclassing", 145 | "swaggerapi", 146 | "tenerowicz", 147 | "testenv", 148 | "testproj", 149 | "therefromhere", 150 | "toctree", 151 | "undoc", 152 | "unencrypted", 153 | "uritemplate", 154 | "urlconf", 155 | "urlconfs", 156 | "urlpatterns", 157 | "versionadded", 158 | "versionchanged", 159 | "versionmodified", 160 | "viewcode", 161 | "viewset", 162 | "viewsets", 163 | "vigrond", 164 | "vschema", 165 | "whitenoise", 166 | "wsgi", 167 | "xdist", 168 | "yasg", 169 | "yasgdoc", 170 | "yetanother", 171 | "yetanothers", 172 | "yourapp", 173 | "yusupov", 174 | "zbyszek" 175 | ] 176 | } 177 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.rst] 14 | indent_style = space 15 | indent_size = 3 16 | 17 | [{package.json,package-lock.json}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.{yml,yaml}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [Makefile] 26 | indent_style = tab 27 | 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: Report a bug 4 | --- 5 | 6 | # Bug Report 7 | 8 | ## Description 9 | 10 | A clear and concise description of the problem... 11 | 12 | ## Is this a regression? 13 | 14 | 15 | Yes, the previous version in which this bug was not present was: ... 16 | 17 | ## Minimal Reproduction 18 | 19 | ```code 20 | 21 | ``` 22 | 23 | ## Stack trace / Error message 24 | 25 | ```code 26 | 27 | ``` 28 | 29 | 30 | 31 | ## Your Environment 32 | 33 | ```code 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature Request' 3 | about: Suggest a feature 4 | --- 5 | 6 | # Feature Request 7 | 8 | ## Description 9 | 10 | A clear and concise description of the problem or missing capability... 11 | 12 | ## Describe the solution you'd like 13 | 14 | If you have a solution in mind, please describe it. 15 | 16 | ## Describe alternatives you've considered 17 | 18 | Have you considered any alternative solutions or workarounds? 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/general.md: -------------------------------------------------------------------------------- 1 | # PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our contributing guidelines 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | - [ ] Bugfix 14 | - [ ] Feature 15 | - [ ] Code style update 16 | - [ ] Refactoring (no functional changes) 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Documentation content changes 20 | - [ ] Other 21 | 22 | ## What is the current behavior? 23 | 24 | ## What is the new behavior? 25 | 26 | ## Does this PR introduce a breaking change? 27 | 28 | - [ ] Yes 29 | - [ ] No 30 | 31 | ## Other information 32 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Install 2 | description: Install dependencies 3 | 4 | inputs: 5 | python-version: 6 | description: Python version for installing dependencies 7 | required: true 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Checkout the source code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set the python version 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ inputs.python-version }} 19 | 20 | - name: Set up pip package caching 21 | uses: actions/cache@v4 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 25 | restore-keys: ${{ runner.os }}-pip- 26 | 27 | - name: Install dependencies 28 | shell: bash 29 | run: pip install -r requirements/ci.txt 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | publish: 10 | name: Publish the package on pypi 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the source code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set the python version 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.11 20 | 21 | - name: Install dependencies 22 | uses: ./.github/actions/install 23 | with: 24 | python-version: 3.11 25 | 26 | - name: Install builders for publishing 27 | run: pip install -r requirements/publish.txt 28 | 29 | - name: Build the distributions 30 | run: python setup.py sdist bdist_wheel 31 | 32 | - name: Publish the package 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run linters and tests 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python: ["3.8", "3.9", "3.10", "3.11", "3.12"] 13 | 14 | steps: 15 | - name: Checkout the source code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set the python version 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python }} 22 | 23 | - name: Install dependencies 24 | uses: ./.github/actions/install 25 | with: 26 | python-version: ${{ matrix.python }} 27 | 28 | - name: Run tests 29 | run: tox -e $(tox -l | grep py${PYTHON_VERSION//.} | paste -sd "," -) 30 | env: 31 | PYTHON_VERSION: ${{ matrix.python }} 32 | 33 | - name: Report coverage 34 | run: | 35 | pip install coverage 36 | coverage report 37 | 38 | - name: Check for incompatibilities with publishing to PyPi 39 | if: ${{ matrix.python == 3.12 }} 40 | run: | 41 | pip install -r requirements/publish.txt 42 | python setup.py sdist 43 | twine check dist/* 44 | 45 | # Return successful statuses "Run linters and tests" for python {3.6...8} 46 | # These are required for branch protection rules that cannot be removed yet 47 | # https://github.com/axnsan12/drf-yasg/pull/922#issuecomment-2739945046 48 | review: 49 | name: Run linters and tests 50 | runs-on: ubuntu-latest 51 | strategy: 52 | matrix: 53 | python: ["3.6", "3.7", "3.8", "3.9"] 54 | steps: 55 | - run: exit 0 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | testproj/db.sqlite3 4 | testproj/staticfiles 5 | \.pytest_cache/ 6 | docs/\.doctrees/ 7 | pip-wheel-metadata/ 8 | 9 | # Created by .ignore support plugin (hsz.mobi) 10 | ### Python template 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | .static_storage/ 66 | .media/ 67 | local_settings.py 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | ### JetBrains template 116 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 117 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 118 | 119 | # User-specific stuff: 120 | .idea/**/workspace.xml 121 | .idea/**/tasks.xml 122 | .idea/dictionaries 123 | 124 | # Sensitive or high-churn files: 125 | .idea/**/dataSources/ 126 | .idea/**/dataSources.ids 127 | .idea/**/dataSources.xml 128 | .idea/**/dataSources.local.xml 129 | .idea/**/sqlDataSources.xml 130 | .idea/**/dynamic.xml 131 | .idea/**/uiDesigner.xml 132 | 133 | # Gradle: 134 | .idea/**/gradle.xml 135 | .idea/**/libraries 136 | 137 | # CMake 138 | cmake-build-debug/ 139 | 140 | # Mongo Explorer plugin: 141 | .idea/**/mongoSettings.xml 142 | 143 | ## File-based project format: 144 | *.iws 145 | 146 | ## Plugin-specific files: 147 | 148 | # IntelliJ 149 | out/ 150 | 151 | # mpeltonen/sbt-idea plugin 152 | .idea_modules/ 153 | 154 | # JIRA plugin 155 | atlassian-ide-plugin.xml 156 | 157 | # Cursive Clojure plugin 158 | .idea/replstate.xml 159 | 160 | # Crashlytics plugin (for Android Studio and IntelliJ) 161 | com_crashlytics_export_strings.xml 162 | crashlytics.properties 163 | crashlytics-build.properties 164 | fabric.properties 165 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | formats: 7 | - pdf 8 | 9 | build: 10 | image: latest 11 | 12 | python: 13 | version: 3.8 14 | install: 15 | - requirements: requirements/docs.txt 16 | - method: pip 17 | path: . 18 | extra_requirements: 19 | - validation 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. |br| raw:: html 2 | 3 |
4 | 5 | ############ 6 | Contributing 7 | ############ 8 | 9 | Contributions are always welcome and appreciated! Here are some ways you can contribute. 10 | 11 | ****** 12 | Issues 13 | ****** 14 | 15 | You can and should open an issue for any of the following reasons: 16 | 17 | * you found a bug; steps for reproducing, or a pull request with a failing test case will be greatly appreciated 18 | * you wanted to do something but did not find a way to do it after reading the documentation 19 | * you believe the current way of doing something is more complicated or less elegant than it can be 20 | * a related feature that you want is missing from the package 21 | 22 | Please always check for existing issues before opening a new issue. 23 | 24 | ************* 25 | Pull requests 26 | ************* 27 | 28 | You want to contribute some code? Great! Here are a few steps to get you started: 29 | 30 | #. **Fork the repository on GitHub** 31 | #. **Clone your fork and create a branch for the code you want to add** 32 | #. **Create a new virtualenv and install the package in development mode** 33 | 34 | .. code:: console 35 | 36 | $ python -m venv venv 37 | $ source venv/bin/activate 38 | (venv) $ python -m pip install -U pip setuptools 39 | (venv) $ pip install -U -e '.[validation]' 40 | (venv) $ pip install -U -r requirements/dev.txt 41 | 42 | #. **Make your changes and check them against the test project** 43 | 44 | .. code:: console 45 | 46 | (venv) $ cd testproj 47 | (venv) $ python manage.py migrate 48 | (venv) $ python manage.py runserver 49 | (venv) $ firefox localhost:8000/swagger/ 50 | 51 | #. **Update the tests if necessary** 52 | 53 | You can find them in the ``tests`` directory. 54 | 55 | If your change modifies the expected schema output, you should regenerate the reference schema at 56 | ``tests/reference.yaml``: 57 | 58 | .. code:: console 59 | 60 | (venv) $ python testproj/manage.py generate_swagger tests/reference.yaml --overwrite --user admin --url http://test.local:8002/ 61 | 62 | After checking the git diff to verify that no unexpected changes appeared, you should commit the new 63 | ``reference.yaml`` together with your changes. 64 | 65 | #. **Run tests. The project is setup to use tox and pytest for testing** 66 | 67 | .. code:: console 68 | 69 | # install test dependencies 70 | (venv) $ pip install -U -r requirements/test.txt 71 | # run tests in the current environment, faster than tox 72 | (venv) $ pytest -n auto --cov 73 | # (optional) sort imports with isort and check flake8 linting 74 | (venv) $ isort --apply 75 | (venv) $ flake8 src/drf_yasg testproj tests setup.py 76 | # (optional) run tests for other python versions in separate environments 77 | (venv) $ tox 78 | 79 | #. **Update documentation** 80 | 81 | If the change modifies behavior or adds new features, you should update the documentation and ``README.rst`` 82 | accordingly. Documentation is written in reStructuredText and built using Sphinx. You can find the sources in the 83 | ``docs`` directory. 84 | 85 | To build and check the docs, run 86 | 87 | .. code:: console 88 | 89 | (venv) $ tox -e docs 90 | 91 | #. **Push your branch and submit a pull request to the master branch on GitHub** 92 | 93 | Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more 94 | easily. 95 | 96 | #. **Your code must pass all the required CI jobs before it is merged** 97 | 98 | As of now, this consists of running on the supported Python, Django, DRF version matrix (see README), 99 | and building the docs successfully. 100 | 101 | ****************** 102 | Maintainer's notes 103 | ****************** 104 | 105 | Release checklist 106 | ================= 107 | 108 | * update ``docs/changelog.rst`` with changes since the last tagged version 109 | * commit & tag the release - ``git tag x.x.x -m "Release version x.x.x"`` 110 | * push using ``git push --follow-tags`` 111 | * verify that `Actions`_ has built the tag and successfully published the release to `PyPI`_ 112 | * publish release notes `on GitHub`_ 113 | * start the `ReadTheDocs build`_ if it has not already started 114 | * deploy the live demo `on Heroku`_ 115 | 116 | 117 | .. _Actions: https://github.com/axnsan12/drf-yasg/actions 118 | .. _PyPI: https://pypi.org/project/drf-yasg/ 119 | .. _on GitHub: https://github.com/axnsan12/drf-yasg/releases 120 | .. _ReadTheDocs build: https://readthedocs.org/projects/drf-yasg/builds/ 121 | .. _on Heroku: https://dashboard.heroku.com/pipelines/412d1cae-6a95-4f5e-810b-94869133f36a 122 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | .. |br| raw:: html 2 | 3 |
4 | 5 | ####### 6 | License 7 | ####### 8 | 9 | ******************** 10 | BSD 3-Clause License 11 | ******************** 12 | 13 | Copyright (c) 2017 - 2019, Cristian V. |br|\ All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are met: 17 | 18 | * Redistributions of source code must retain the above copyright notice, this 19 | list of conditions and the following disclaimer. 20 | 21 | * Redistributions in binary form must reproduce the above copyright notice, 22 | this list of conditions and the following disclaimer in the documentation 23 | and/or other materials provided with the distribution. 24 | 25 | * Neither the name of the copyright holder nor the names of its 26 | contributors may be used to endorse or promote products derived from 27 | this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 30 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 31 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 33 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 34 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 35 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 36 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 37 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 38 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.rst 3 | include pyproject.toml 4 | recursive-include requirements * 5 | recursive-include src/drf_yasg/static * 6 | recursive-include src/drf_yasg/templates * 7 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python testproj/manage.py migrate 2 | web: gunicorn --chdir testproj testproj.wsgi --log-file - 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drf-yasg Demo app", 3 | "description": "A demonstrative app using https://github.com/axnsan12/drf-yasg", 4 | "repository": "https://github.com/axnsan12/drf-yasg", 5 | "logo": "https://swaggerhub.com/wp-content/uploads/2017/10/Swagger-Icon.svg", 6 | "keywords": [ 7 | "django", 8 | "django-rest-framework", 9 | "swagger", 10 | "openapi" 11 | ], 12 | "env": { 13 | "DJANGO_SETTINGS_MODULE": "testproj.settings.heroku", 14 | "DJANGO_SECRET_KEY": "m76=^#=z7xv5^(o%4dv9w7+1_c)y2m6)1ogjx%s@9$1^nupry=" 15 | }, 16 | "success_url": "/" 17 | } 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = drf-yasg 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # force directory to show up in git 2 | -------------------------------------------------------------------------------- /docs/_static/css/style.css: -------------------------------------------------------------------------------- 1 | .versionadded, .versionchanged, .deprecated { 2 | font-family: "Roboto", Corbel, Avenir, "Lucida Grande", "Lucida Sans", sans-serif; 3 | padding: 10px 13px; 4 | border: 1px solid rgb(137, 191, 4); 5 | border-radius: 4px; 6 | margin-bottom: 10px; 7 | } 8 | 9 | .versionmodified { 10 | font-weight: bold; 11 | display: block; 12 | } 13 | 14 | .versionadded p, .versionchanged p, .deprecated p, 15 | /*override fucking !important by being more specific */ 16 | .rst-content dl .versionadded p, .rst-content dl .versionchanged p { 17 | margin: 0 !important; 18 | } 19 | 20 | .align-center { 21 | width: 100% !important; 22 | background-color: #000000; 23 | } 24 | -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- 1 | # force directory to show up in git 2 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% block extrahead %} 3 | 4 | 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/custom_ui.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | Customizing the web UI 3 | ###################### 4 | 5 | The web UI can be customized using the settings available in :ref:`swagger-ui-settings` and :ref:`redoc-ui-settings`. 6 | 7 | You can also extend one of the `drf-yasg/swagger-ui.html`_ or `drf-yasg/redoc.html`_ templates that are used for 8 | rendering. See the template source code (linked above) for a complete list of customizable blocks. 9 | 10 | The ``swagger-ui`` view has some quite involved JavaScript hooks used for some functionality, which you might also 11 | want to review at `drf-yasg/swagger-ui-init.js`_. 12 | 13 | .. _drf-yasg/swagger-ui.html: https://github.com/axnsan12/drf-yasg/blob/master/src/drf_yasg/templates/drf-yasg/swagger-ui.html 14 | .. _drf-yasg/swagger-ui-init.js: https://github.com/axnsan12/drf-yasg/blob/master/src/drf_yasg/static/drf-yasg/swagger-ui-init.js 15 | .. _drf-yasg/redoc.html: https://github.com/axnsan12/drf-yasg/blob/master/src/drf_yasg/templates/drf-yasg/redoc.html 16 | -------------------------------------------------------------------------------- /docs/drf_yasg.rst: -------------------------------------------------------------------------------- 1 | drf\_yasg package 2 | ==================== 3 | 4 | drf\_yasg\.codecs 5 | --------------------------- 6 | 7 | .. automodule:: drf_yasg.codecs 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | :exclude-members: SaneYamlDumper,SaneYamlLoader 12 | 13 | drf\_yasg\.errors 14 | --------------------------- 15 | 16 | .. automodule:: drf_yasg.errors 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | drf\_yasg\.generators 22 | ------------------------------- 23 | 24 | .. automodule:: drf_yasg.generators 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | drf\_yasg\.inspectors 30 | ------------------------------- 31 | 32 | .. autodata:: drf_yasg.inspectors.NotHandled 33 | 34 | .. automodule:: drf_yasg.inspectors 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | drf\_yasg\.middleware 40 | ------------------------------- 41 | 42 | .. automodule:: drf_yasg.middleware 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | drf\_yasg\.openapi 48 | ---------------------------- 49 | 50 | .. automodule:: drf_yasg.openapi 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | :exclude-members: _bare_SwaggerDict 55 | 56 | drf\_yasg\.renderers 57 | ------------------------------ 58 | 59 | .. automodule:: drf_yasg.renderers 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | drf\_yasg\.utils 65 | -------------------------- 66 | 67 | .. automodule:: drf_yasg.utils 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | drf\_yasg\.views 73 | -------------------------- 74 | 75 | .. automodule:: drf_yasg.views 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | .. |br| raw:: html 81 | 82 |
83 | -------------------------------------------------------------------------------- /docs/images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/docs/images/flow.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. drf-yasg documentation master file, created by 2 | sphinx-quickstart on Sun Dec 10 15:20:34 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | drf-yasg 7 | =========== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Table of contents: 12 | 13 | readme.rst 14 | rendering.rst 15 | openapi.rst 16 | security.rst 17 | custom_spec.rst 18 | custom_ui.rst 19 | settings.rst 20 | contributing.rst 21 | license.rst 22 | changelog.rst 23 | 24 | Source code documentation 25 | ========================= 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | .. toctree:: 32 | :maxdepth: 2 33 | 34 | drf_yasg.rst 35 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../LICENSE.rst -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=drf-yasg 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst -------------------------------------------------------------------------------- /docs/rendering.rst: -------------------------------------------------------------------------------- 1 | ################## 2 | Serving the schema 3 | ################## 4 | 5 | 6 | ************************************************ 7 | ``get_schema_view`` and the ``SchemaView`` class 8 | ************************************************ 9 | 10 | The :func:`.get_schema_view` function and the :class:`.SchemaView` class it returns (click links for documentation) 11 | are intended to cover the majority of use cases one might want to configure. The class returned by 12 | :func:`.get_schema_view` can be used to obtain view instances via :meth:`.SchemaView.with_ui`, 13 | :meth:`.SchemaView.without_ui` and :meth:`.SchemaView.as_cached_view` - see :ref:`readme-quickstart` 14 | in the README for a usage example. 15 | 16 | You can also subclass :class:`.SchemaView` by extending the return value of :func:`.get_schema_view`, e.g.: 17 | 18 | .. code-block:: python 19 | 20 | SchemaView = get_schema_view(info, ...) 21 | 22 | class CustomSchemaView(SchemaView): 23 | generator_class = CustomSchemaGenerator 24 | renderer_classes = (CustomRenderer1, CustomRenderer2,) 25 | 26 | ******************** 27 | Renderers and codecs 28 | ******************** 29 | 30 | If you need to modify how your Swagger spec is presented in views, you might want to override one of the renderers in 31 | :mod:`.renderers` or one of the codecs in :mod:`.codecs`. The codec is the last stage where the Swagger object 32 | arrives before being transformed into bytes, while the renderer is the stage responsible for tying together the 33 | codec and the view. 34 | 35 | You can use your custom renderer classes as kwargs to :meth:`.SchemaView.as_cached_view` or by subclassing 36 | :class:`.SchemaView`. 37 | 38 | .. _management-command: 39 | 40 | ****************** 41 | Management command 42 | ****************** 43 | 44 | If you only need a swagger spec file in YAML or JSON format, you can use the ``generate_swagger`` management command 45 | to get it without having to start the web server: 46 | 47 | .. code-block:: console 48 | 49 | $ python manage.py generate_swagger swagger.json 50 | 51 | See the command help for more advanced options: 52 | 53 | .. code-block:: console 54 | 55 | $ python manage.py generate_swagger --help 56 | usage: manage.py generate_swagger [-h] [--version] [-v {0,1,2,3}] 57 | ... more options ... 58 | 59 | 60 | .. Note:: 61 | 62 | The :ref:`DEFAULT_INFO ` setting must be defined when using the ``generate_swagger`` 63 | command. For example, the :ref:`README quickstart ` code could be modified as such: 64 | 65 | In ``settings.py``: 66 | 67 | .. code-block:: python 68 | 69 | SWAGGER_SETTINGS = { 70 | 'DEFAULT_INFO': 'import.path.to.urls.api_info', 71 | } 72 | 73 | In ``urls.py``: 74 | 75 | .. code-block:: python 76 | 77 | api_info = openapi.Info( 78 | title="Snippets API", 79 | ... other arguments ... 80 | ) 81 | 82 | schema_view = get_schema_view( 83 | # the info argument is no longer needed here as it will be picked up from DEFAULT_INFO 84 | ... other arguments ... 85 | ) 86 | -------------------------------------------------------------------------------- /docs/security.rst: -------------------------------------------------------------------------------- 1 | ********************************* 2 | Describing authentication schemes 3 | ********************************* 4 | 5 | When using the `swagger-ui` frontend, it is possible to interact with the API described by your Swagger document. 6 | This interaction might require authentication, which you will have to describe in order to make `swagger-ui` work 7 | with it. 8 | 9 | 10 | -------------------- 11 | Security definitions 12 | -------------------- 13 | 14 | The first step that you have to do is add a :ref:`SECURITY_DEFINITIONS ` setting 15 | to declare all authentication schemes supported by your API. 16 | 17 | For example, the definition for a simple API accepting HTTP basic auth and `Authorization` header API tokens would be: 18 | 19 | .. code-block:: python 20 | 21 | SWAGGER_SETTINGS = { 22 | 'SECURITY_DEFINITIONS': { 23 | 'Basic': { 24 | 'type': 'basic' 25 | }, 26 | 'Bearer': { 27 | 'type': 'apiKey', 28 | 'name': 'Authorization', 29 | 'in': 'header' 30 | } 31 | } 32 | } 33 | 34 | 35 | --------------------- 36 | Security requirements 37 | --------------------- 38 | 39 | The second step is specifying, for each endpoint, which authentication mechanism can be used for interacting with it. 40 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-requirement-object for details. 41 | 42 | By default, a top-level `security` that accepts any one of the declared security definitions is generated. 43 | For the example above, that would be :code:`[{'Basic': []}, {'Bearer': []}]`. This can be overridden using the 44 | :ref:`SECURITY_REQUIREMENTS ` setting. 45 | 46 | Operation-level overrides can be added using the ``security`` parameter of 47 | :ref:`@swagger_auto_schema `. 48 | 49 | 50 | ------------------------------- 51 | ``swagger-ui`` as OAuth2 client 52 | ------------------------------- 53 | 54 | It is possible to configure ``swagger-ui`` to authenticate against your (or a third party) OAuth2 service when sending 55 | "Try it out" requests. This client-side configuration does not remove the requirement of a spec-side 56 | :ref:`security definition `, but merely allows you to test OAuth2 APIs using 57 | ``swagger-ui`` as a client. 58 | 59 | **DISCLAIMER**: this setup is very poorly tested as I do not currently implement OAuth in any of my projects. All 60 | contributions relating to documentation, bugs, mistakes or anything else are welcome as an issue or pull request. The 61 | settings described below were added as a result of discussion in issue :issue:`53`. 62 | 63 | The settings of interest can be found on the :ref:`settings page `. Configuration options are similar 64 | to most OAuth client setups like web or mobile applications. Reading the relevant ``swagger-ui`` documentation linked 65 | will also probably help. 66 | 67 | 68 | Example 69 | ^^^^^^^ 70 | 71 | A very simple working configuration was provided by :ghuser:`Vigrond`, originally at 72 | `https://github.com/Vigrond/django_oauth2_example `_. 73 | 74 | 75 | .. code-block:: python 76 | 77 | SWAGGER_SETTINGS = { 78 | 'USE_SESSION_AUTH': False, 79 | 'SECURITY_DEFINITIONS': { 80 | 'Your App API - Swagger': { 81 | 'type': 'oauth2', 82 | 'authorizationUrl': '/yourapp/o/authorize', 83 | 'tokenUrl': '/yourapp/o/token/', 84 | 'flow': 'accessCode', 85 | 'scopes': { 86 | 'read:groups': 'read groups', 87 | } 88 | } 89 | }, 90 | 'OAUTH2_CONFIG': { 91 | 'clientId': 'yourAppClientId', 92 | 'clientSecret': 'yourAppClientSecret', 93 | 'appName': 'your application name' 94 | }, 95 | } 96 | 97 | If the OAuth2 provider requires you to provide the full absolute redirect URL, the default value for most 98 | ``staticfiles`` configurations will be ``/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html``. If this is 99 | not suitable for some reason, you can override the ``OAUTH2_REDIRECT_URL`` setting as appropriate. 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "redoc": "^2.0.0-rc.59", 4 | "swagger-ui-dist": "^5.18.2" 5 | }, 6 | "devDependencies": { 7 | "cspell": "^6.30.0", 8 | "luxon": "^3.5.0", 9 | "octokit": "^4.1.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | 4 | requires = [ "setuptools>=68", "setuptools-scm>=3.0.3", "wheel" ] 5 | 6 | [tool.ruff] 7 | exclude = [ "**/migrations/*" ] 8 | line-length = 88 9 | lint.select = [ 10 | "AIR", # Airflow 11 | "ASYNC", # flake8-async 12 | "C4", # flake8-comprehensions 13 | "C90", # mccabe 14 | "E", # pycodestyle errors 15 | "F", # Pyflakes 16 | "FA", # flake8-future-annotations 17 | "FLY", # flynt 18 | "I", # isort 19 | "ICN", # flake8-import-conventions 20 | "INT", # flake8-gettext 21 | "LOG", # flake8-logging 22 | "NPY", # NumPy-specific rules 23 | "PD", # pandas-vet 24 | "PLE", # Pylint errors 25 | "PYI", # flake8-pyi 26 | "SLOT", # flake8-slots 27 | "T10", # flake8-debugger 28 | "TCH", # flake8-type-checking 29 | "W", # pycodestyle warnings 30 | "YTT", # flake8-2020 31 | ] 32 | lint.isort.known-first-party = [ "drf_yasg", "testproj", "articles", "people", "snippets", "todo", "users", "urlconfs" ] 33 | lint.mccabe.max-complexity = 13 34 | lint.per-file-ignores."src/drf_yasg/inspectors/field.py" = [ "E721" ] 35 | lint.per-file-ignores."src/drf_yasg/openapi.py" = [ "E721" ] 36 | lint.per-file-ignores."testproj/testproj/settings/*" = [ "F405" ] 37 | 38 | [tool.setuptools_scm] 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # this file is only used when deploying to heroku, because heroku insists on having a root-level requirements.txt 2 | # for normal usage see the requirements/ directory 3 | .[validation] 4 | -r requirements/heroku.txt 5 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | djangorestframework>=3.10.3 2 | django>=2.2.16 3 | pyyaml>=5.1 4 | inflection>=0.3.1 5 | packaging>=21.0 6 | pytz>=2021.1 7 | uritemplate>=3.0.0 8 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | # requirements for the CI test runner 2 | codecov>=2.0.9 3 | 4 | -r tox.txt 5 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # requirements for local development to be installed via pip install -U -r requirements/dev.txt 2 | -r tox.txt 3 | -r test.txt 4 | -r lint.txt 5 | 6 | tox-battery>=0.5 7 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # used by the 'docs' tox env for building the documentation 2 | Sphinx>=1.7.0 3 | sphinx_rtd_theme>=0.2.4 4 | Pillow>=4.3.0 5 | readme_renderer[md]>=24.0 6 | twine>=1.12.1 7 | 8 | Django>=2.0 9 | djangorestframework_camel_case>=0.2.0 10 | -------------------------------------------------------------------------------- /requirements/heroku.txt: -------------------------------------------------------------------------------- 1 | # requirements necessary when deploying the test project to heroku 2 | -r testproj.txt 3 | psycopg2>=2.7.3 4 | gunicorn>=19.7.1 5 | whitenoise>=3.3.1 6 | -------------------------------------------------------------------------------- /requirements/lint.txt: -------------------------------------------------------------------------------- 1 | # used by the 'lint' tox env for linting via ruff 2 | ruff>=0.11.0 3 | -------------------------------------------------------------------------------- /requirements/publish.txt: -------------------------------------------------------------------------------- 1 | setuptools-scm==7.0.5 2 | twine>=5.0.0 3 | wheel>=0.37.0 4 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # requirements for running the tests via pytest 2 | pytest>=4.0 3 | pytest-pythonpath>=0.7.1 4 | pytest-cov>=2.6.0 5 | pytest-xdist>=1.25.0 6 | pytest-django>=3.4.4 7 | datadiff==2.0.0 8 | psycopg2-binary==2.9.5 9 | django-fake-model==0.1.4 10 | 11 | -r testproj.txt 12 | -------------------------------------------------------------------------------- /requirements/testproj.txt: -------------------------------------------------------------------------------- 1 | # test project requirements 2 | Pillow>=4.3.0 3 | django-filter>=2.4.0,<25 4 | djangorestframework-camel-case>=1.1.2 5 | djangorestframework-recursive>=0.1.2 6 | dj-database-url>=0.4.2 7 | user_agents>=1.1.0 8 | django-cors-headers 9 | django-oauth-toolkit>=1.3.0 10 | 11 | -------------------------------------------------------------------------------- /requirements/tox.txt: -------------------------------------------------------------------------------- 1 | # requirements for building and running tox 2 | tox>=3.3.0,<4 3 | -------------------------------------------------------------------------------- /requirements/validation.txt: -------------------------------------------------------------------------------- 1 | # requirements for the validation feature 2 | swagger-spec-validator>=2.1.0 3 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.0 2 | -------------------------------------------------------------------------------- /screenshots/redoc-nested-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/screenshots/redoc-nested-response.png -------------------------------------------------------------------------------- /screenshots/swagger-ui-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/screenshots/swagger-ui-list.png -------------------------------------------------------------------------------- /screenshots/swagger-ui-models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/screenshots/swagger-ui-models.png -------------------------------------------------------------------------------- /scripts/linters/labels.js: -------------------------------------------------------------------------------- 1 | import { DateTime, Duration } from "luxon"; 2 | import { Octokit } from "octokit"; 3 | 4 | const token = process.env.GITHUB_TOKEN; 5 | const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); 6 | const { rest, paginate } = new Octokit({ auth: token }); 7 | 8 | const now = DateTime.now(); 9 | 10 | (async () => { 11 | for await (const { data: issues } of paginate.iterator( 12 | rest.issues.listForRepo, 13 | { owner, repo } 14 | )) { 15 | issues.forEach((issue) => { 16 | const { number, created_at, title, pull_request, html_url } = issue; 17 | const assignee = issue.assignee?.login; 18 | 19 | const age = now 20 | .diff(DateTime.fromISO(created_at)) 21 | .shiftTo("years", "months", "days") 22 | .toHuman({ maximumFractionDigits: 0 }); 23 | 24 | const labels = issue.labels.map(({ name }) => name); 25 | const version = labels.find((name) => /\d+\.[\dx]+\.[\dx]+/.test(name)); 26 | 27 | const triage = labels.includes("triage"); 28 | const help = labels.includes("help wanted"); 29 | 30 | const question = labels.includes("question"); 31 | const unanswered = labels.includes("unanswered"); 32 | 33 | const directions = labels.filter((i) => 34 | ["bug", "enhancement", "question"].includes(i) 35 | ); 36 | 37 | const problems = []; 38 | 39 | if (version) { 40 | if (assignee && help) { 41 | problems.push( 42 | 'Should not have both an assignee and a "help wanted" label' 43 | ); 44 | } 45 | 46 | if (!assignee && !help) { 47 | problems.push('Missing an assignee or a "help wanted" label'); 48 | } 49 | } else { 50 | if (assignee || help) { 51 | problems.push("Missing a version label"); 52 | } 53 | } 54 | 55 | if (pull_request) { 56 | if (triage && version) { 57 | problems.push('Should not have both a "triage" and version label'); 58 | } 59 | 60 | if (!triage && !version) { 61 | problems.push('Missing a "triage" or version label'); 62 | } 63 | } else { 64 | if ([triage, question, version].filter((i) => i).length > 1) { 65 | problems.push('Too many "triage", "question" and version labels'); 66 | } 67 | 68 | if ([triage, question, version].every((i) => !i)) { 69 | problems.push('Missing a "triage", "question" or version label'); 70 | } 71 | } 72 | 73 | if (!triage) { 74 | if (directions.length === 0) { 75 | problems.push('Missing a "bug", "enhancement" or "question" label'); 76 | } 77 | 78 | if (directions.length > 1) { 79 | problems.push('Too many "bug", "enhancement" and "question" labels'); 80 | } 81 | } 82 | 83 | if (problems.length > 0) { 84 | console.log({ 85 | age, 86 | source: { 87 | id: number, 88 | title, 89 | url: html_url, 90 | }, 91 | problems, 92 | context: { 93 | labels, 94 | assignee, 95 | version, 96 | help_wanted: help, 97 | triage, 98 | question, 99 | unanswered, 100 | directions, 101 | }, 102 | }); 103 | } 104 | }); 105 | } 106 | })(); 107 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE.rst 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import io 6 | import os 7 | import sys 8 | 9 | from setuptools import find_packages, setup 10 | 11 | 12 | def read_req(req_file): 13 | with open(os.path.join("requirements", req_file)) as req: 14 | return [ 15 | line.strip() 16 | for line in req.readlines() 17 | if line.strip() and not line.strip().startswith("#") 18 | ] 19 | 20 | 21 | with io.open("README.rst", encoding="utf-8") as readme: 22 | description = readme.read() 23 | 24 | requirements = read_req("base.txt") 25 | requirements_validation = read_req("validation.txt") 26 | 27 | 28 | def find_versions_from_readme(prefix): 29 | for line in description.splitlines(): 30 | line = line.strip() 31 | if line.startswith(prefix): 32 | versions = [v.strip() for v in line[len(prefix) :].split(",")] 33 | if versions: 34 | return versions 35 | 36 | raise RuntimeError("failed to find supported versions list for '{}'".format(prefix)) 37 | 38 | 39 | python_versions = find_versions_from_readme("- **Python**: ") 40 | django_versions = find_versions_from_readme("- **Django**: ") 41 | 42 | python_requires = ">=" + python_versions[0] 43 | 44 | python_classifiers = [ 45 | "Programming Language :: Python", 46 | "Programming Language :: Python :: 3", 47 | ] + ["Programming Language :: Python :: {}".format(v) for v in python_versions] 48 | django_classifiers = [ 49 | "Framework :: Django", 50 | ] + ["Framework :: Django :: {}".format(v) for v in django_versions] 51 | 52 | 53 | def drf_yasg_setup(**kwargs): 54 | setup( 55 | name="drf-yasg", 56 | packages=find_packages("src"), 57 | package_dir={"": "src"}, 58 | include_package_data=True, 59 | install_requires=requirements, 60 | extras_require={ 61 | "validation": requirements_validation, 62 | "coreapi": ["coreapi>=2.3.3", "coreschema>=0.0.4"], 63 | }, 64 | license="BSD License", 65 | description="Automated generation of real Swagger/OpenAPI 2.0 schemas from " 66 | "Django Rest Framework code.", 67 | long_description=description, 68 | long_description_content_type="text/x-rst", 69 | url="https://github.com/axnsan12/drf-yasg", 70 | author="Cristi V.", 71 | author_email="cristi@cvjd.me", 72 | keywords="drf django django-rest-framework schema swagger openapi codegen " 73 | "swagger-codegen documentation drf-yasg django-rest-swagger drf-openapi", 74 | python_requires=python_requires, 75 | classifiers=[ 76 | "Intended Audience :: Developers", 77 | "License :: OSI Approved :: BSD License", 78 | "Development Status :: 5 - Production/Stable", 79 | "Operating System :: OS Independent", 80 | "Environment :: Web Environment", 81 | "Topic :: Documentation", 82 | "Topic :: Software Development :: Code Generators", 83 | ] 84 | + python_classifiers 85 | + django_classifiers, 86 | **kwargs, 87 | ) 88 | 89 | 90 | try: 91 | # noinspection PyUnresolvedReferences 92 | import setuptools_scm # noqa: F401 93 | 94 | drf_yasg_setup(use_scm_version=True) 95 | except (ImportError, LookupError) as e: 96 | if os.getenv("CI", "false") == "true": 97 | # don't silently fail on CI - we don't want to accidentally push a dummy version 98 | # to PyPI 99 | raise 100 | 101 | err_msg = str(e) 102 | if "setuptools-scm" in err_msg or "setuptools_scm" in err_msg: 103 | import time 104 | import traceback 105 | 106 | timestamp_ms = int(time.time() * 1000) 107 | timestamp_str = hex(timestamp_ms)[2:].zfill(16) 108 | dummy_version = "1!0.0.0.dev0+noscm." + timestamp_str 109 | 110 | drf_yasg_setup(version=dummy_version) 111 | 112 | traceback.print_exc(file=sys.stderr) 113 | print( 114 | "failed to detect version, package was built with dummy version " 115 | + dummy_version, 116 | file=sys.stderr, 117 | ) 118 | else: 119 | raise 120 | -------------------------------------------------------------------------------- /src/drf_yasg/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | __author__ = """Cristi V.""" 4 | __email__ = "cristi@cvjd.me" 5 | 6 | try: 7 | from importlib.metadata import version 8 | 9 | __version__ = version(__name__) 10 | except ImportError: # Python < 3.8 11 | try: 12 | from pkg_resources import DistributionNotFound, get_distribution 13 | 14 | __version__ = get_distribution(__name__).version 15 | except DistributionNotFound: # pragma: no cover 16 | # package is not installed 17 | pass 18 | -------------------------------------------------------------------------------- /src/drf_yasg/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.settings import perform_import 3 | 4 | SWAGGER_DEFAULTS = { 5 | "DEFAULT_GENERATOR_CLASS": "drf_yasg.generators.OpenAPISchemaGenerator", 6 | "DEFAULT_AUTO_SCHEMA_CLASS": "drf_yasg.inspectors.SwaggerAutoSchema", 7 | "DEFAULT_FIELD_INSPECTORS": [ 8 | "drf_yasg.inspectors.CamelCaseJSONFilter", 9 | "drf_yasg.inspectors.RecursiveFieldInspector", 10 | "drf_yasg.inspectors.ReferencingSerializerInspector", 11 | "drf_yasg.inspectors.ChoiceFieldInspector", 12 | "drf_yasg.inspectors.FileFieldInspector", 13 | "drf_yasg.inspectors.DictFieldInspector", 14 | "drf_yasg.inspectors.JSONFieldInspector", 15 | "drf_yasg.inspectors.HiddenFieldInspector", 16 | "drf_yasg.inspectors.RelatedFieldInspector", 17 | "drf_yasg.inspectors.SerializerMethodFieldInspector", 18 | "drf_yasg.inspectors.SimpleFieldInspector", 19 | "drf_yasg.inspectors.StringDefaultFieldInspector", 20 | ], 21 | "DEFAULT_FILTER_INSPECTORS": [ 22 | "drf_yasg.inspectors.DrfAPICompatInspector", 23 | "drf_yasg.inspectors.CoreAPICompatInspector", 24 | ], 25 | "DEFAULT_PAGINATOR_INSPECTORS": [ 26 | "drf_yasg.inspectors.DjangoRestResponsePagination", 27 | "drf_yasg.inspectors.DrfAPICompatInspector", 28 | "drf_yasg.inspectors.CoreAPICompatInspector", 29 | ], 30 | "DEFAULT_SPEC_RENDERERS": [ 31 | "drf_yasg.renderers.SwaggerYAMLRenderer", 32 | "drf_yasg.renderers.SwaggerJSONRenderer", 33 | "drf_yasg.renderers.OpenAPIRenderer", 34 | ], 35 | "EXCLUDED_MEDIA_TYPES": ["html"], 36 | "DEFAULT_INFO": None, 37 | "DEFAULT_API_URL": None, 38 | "USE_SESSION_AUTH": True, 39 | "USE_COMPAT_RENDERERS": getattr(settings, "SWAGGER_USE_COMPAT_RENDERERS", True), 40 | "CSRF_COOKIE_NAME": settings.CSRF_COOKIE_NAME, 41 | "CSRF_HEADER_NAME": settings.CSRF_HEADER_NAME, 42 | "SECURITY_DEFINITIONS": {"Basic": {"type": "basic"}}, 43 | "SECURITY_REQUIREMENTS": None, 44 | "LOGIN_URL": getattr(settings, "LOGIN_URL", None), 45 | "LOGOUT_URL": "/accounts/logout/", 46 | "SPEC_URL": None, 47 | "VALIDATOR_URL": "", 48 | "PERSIST_AUTH": False, 49 | "REFETCH_SCHEMA_WITH_AUTH": False, 50 | "REFETCH_SCHEMA_ON_LOGOUT": False, 51 | "FETCH_SCHEMA_WITH_QUERY": True, 52 | "OPERATIONS_SORTER": None, 53 | "TAGS_SORTER": None, 54 | "DOC_EXPANSION": "list", 55 | "DEEP_LINKING": False, 56 | "SHOW_EXTENSIONS": True, 57 | "DEFAULT_MODEL_RENDERING": "model", 58 | "DEFAULT_MODEL_DEPTH": 3, 59 | "SHOW_COMMON_EXTENSIONS": True, 60 | "OAUTH2_REDIRECT_URL": None, 61 | "OAUTH2_CONFIG": {}, 62 | "SUPPORTED_SUBMIT_METHODS": [ 63 | "get", 64 | "put", 65 | "post", 66 | "delete", 67 | "options", 68 | "head", 69 | "patch", 70 | "trace", 71 | ], 72 | "DISPLAY_OPERATION_ID": True, 73 | } 74 | 75 | REDOC_DEFAULTS = { 76 | "SPEC_URL": None, 77 | "LAZY_RENDERING": False, 78 | "HIDE_HOSTNAME": False, 79 | "EXPAND_RESPONSES": "all", 80 | "PATH_IN_MIDDLE": False, 81 | "NATIVE_SCROLLBARS": False, 82 | "REQUIRED_PROPS_FIRST": False, 83 | "FETCH_SCHEMA_WITH_QUERY": True, 84 | "HIDE_DOWNLOAD_BUTTON": False, 85 | } 86 | 87 | IMPORT_STRINGS = [ 88 | "DEFAULT_GENERATOR_CLASS", 89 | "DEFAULT_AUTO_SCHEMA_CLASS", 90 | "DEFAULT_FIELD_INSPECTORS", 91 | "DEFAULT_FILTER_INSPECTORS", 92 | "DEFAULT_PAGINATOR_INSPECTORS", 93 | "DEFAULT_SPEC_RENDERERS", 94 | "DEFAULT_INFO", 95 | ] 96 | 97 | 98 | class AppSettings: 99 | """ 100 | Stolen from Django Rest Framework, removed caching for easier testing 101 | """ 102 | 103 | def __init__(self, user_settings, defaults, import_strings=None): 104 | self._user_settings = user_settings 105 | self.defaults = defaults 106 | self.import_strings = import_strings or [] 107 | 108 | @property 109 | def user_settings(self): 110 | return getattr(settings, self._user_settings, {}) 111 | 112 | def __getattr__(self, attr): 113 | if attr not in self.defaults: 114 | raise AttributeError("Invalid setting: '%s'" % attr) # pragma: no cover 115 | 116 | try: 117 | # Check if present in user settings 118 | val = self.user_settings[attr] 119 | except KeyError: 120 | # Fall back to defaults 121 | val = self.defaults[attr] 122 | 123 | # Coerce import strings into classes 124 | if attr in self.import_strings: 125 | val = perform_import(val, attr) 126 | 127 | return val 128 | 129 | 130 | #: 131 | swagger_settings = AppSettings( 132 | user_settings="SWAGGER_SETTINGS", 133 | defaults=SWAGGER_DEFAULTS, 134 | import_strings=IMPORT_STRINGS, 135 | ) 136 | 137 | #: 138 | redoc_settings = AppSettings( 139 | user_settings="REDOC_SETTINGS", 140 | defaults=REDOC_DEFAULTS, 141 | import_strings=IMPORT_STRINGS, 142 | ) 143 | -------------------------------------------------------------------------------- /src/drf_yasg/errors.py: -------------------------------------------------------------------------------- 1 | class SwaggerError(Exception): 2 | pass 3 | 4 | 5 | class SwaggerValidationError(SwaggerError): 6 | def __init__(self, msg, errors=None, spec=None, source_codec=None, *args): 7 | super(SwaggerValidationError, self).__init__(msg, *args) 8 | self.errors = errors 9 | self.spec = spec 10 | self.source_codec = source_codec 11 | 12 | 13 | class SwaggerGenerationError(SwaggerError): 14 | pass 15 | -------------------------------------------------------------------------------- /src/drf_yasg/inspectors/__init__.py: -------------------------------------------------------------------------------- 1 | from ..app_settings import swagger_settings 2 | from .base import ( 3 | BaseInspector, 4 | FieldInspector, 5 | FilterInspector, 6 | NotHandled, 7 | PaginatorInspector, 8 | SerializerInspector, 9 | ViewInspector, 10 | ) 11 | from .field import ( 12 | CamelCaseJSONFilter, 13 | ChoiceFieldInspector, 14 | DictFieldInspector, 15 | FileFieldInspector, 16 | HiddenFieldInspector, 17 | InlineSerializerInspector, 18 | JSONFieldInspector, 19 | RecursiveFieldInspector, 20 | ReferencingSerializerInspector, 21 | RelatedFieldInspector, 22 | SerializerMethodFieldInspector, 23 | SimpleFieldInspector, 24 | StringDefaultFieldInspector, 25 | ) 26 | from .query import ( 27 | CoreAPICompatInspector, 28 | DjangoRestResponsePagination, 29 | DrfAPICompatInspector, 30 | ) 31 | from .view import SwaggerAutoSchema 32 | 33 | # these settings must be accessed only after defining/importing all the classes in this 34 | # module to avoid ImportErrors 35 | ViewInspector.field_inspectors = swagger_settings.DEFAULT_FIELD_INSPECTORS 36 | ViewInspector.filter_inspectors = swagger_settings.DEFAULT_FILTER_INSPECTORS 37 | ViewInspector.paginator_inspectors = swagger_settings.DEFAULT_PAGINATOR_INSPECTORS 38 | 39 | __all__ = [ 40 | # base inspectors 41 | "BaseInspector", 42 | "FilterInspector", 43 | "PaginatorInspector", 44 | "FieldInspector", 45 | "SerializerInspector", 46 | "ViewInspector", 47 | # filter and pagination inspectors 48 | "DrfAPICompatInspector", 49 | "CoreAPICompatInspector", 50 | "DjangoRestResponsePagination", 51 | # field inspectors 52 | "InlineSerializerInspector", 53 | "RecursiveFieldInspector", 54 | "ReferencingSerializerInspector", 55 | "RelatedFieldInspector", 56 | "SimpleFieldInspector", 57 | "FileFieldInspector", 58 | "ChoiceFieldInspector", 59 | "DictFieldInspector", 60 | "JSONFieldInspector", 61 | "StringDefaultFieldInspector", 62 | "CamelCaseJSONFilter", 63 | "HiddenFieldInspector", 64 | "SerializerMethodFieldInspector", 65 | # view inspectors 66 | "SwaggerAutoSchema", 67 | # module constants 68 | "NotHandled", 69 | ] 70 | -------------------------------------------------------------------------------- /src/drf_yasg/inspectors/query.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import wraps 3 | 4 | try: 5 | import coreschema 6 | except ImportError: 7 | coreschema = None 8 | 9 | from .. import openapi 10 | from ..utils import force_real_str 11 | from .base import FilterInspector, NotHandled, PaginatorInspector 12 | 13 | 14 | def ignore_assert_decorator(func): 15 | @wraps(func) 16 | def wrapper(*args, **kwargs): 17 | try: 18 | return func(*args, **kwargs) 19 | except AssertionError: 20 | return NotHandled 21 | 22 | return wrapper 23 | 24 | 25 | class DrfAPICompatInspector(PaginatorInspector, FilterInspector): 26 | def param_to_schema(self, param): 27 | return openapi.Parameter( 28 | name=param["name"], 29 | in_=param["in"], 30 | description=param.get("description"), 31 | required=param.get("required", False), 32 | **param["schema"], 33 | ) 34 | 35 | def get_paginator_parameters(self, paginator): 36 | if hasattr(paginator, "get_schema_operation_parameters"): 37 | return list( 38 | map( 39 | self.param_to_schema, 40 | paginator.get_schema_operation_parameters(self.view), 41 | ) 42 | ) 43 | return NotHandled 44 | 45 | def get_filter_parameters(self, filter_backend): 46 | if hasattr(filter_backend, "get_schema_operation_parameters"): 47 | return list( 48 | map( 49 | self.param_to_schema, 50 | filter_backend.get_schema_operation_parameters(self.view), 51 | ) 52 | ) 53 | return NotHandled 54 | 55 | 56 | class CoreAPICompatInspector(PaginatorInspector, FilterInspector): 57 | """Converts ``coreapi.Field``\\ s to :class:`.openapi.Parameter`\\ s for filters and 58 | paginators that implement a ``get_schema_fields`` method. 59 | """ 60 | 61 | @ignore_assert_decorator 62 | def get_paginator_parameters(self, paginator): 63 | fields = [] 64 | if hasattr(paginator, "get_schema_fields"): 65 | fields = paginator.get_schema_fields(self.view) 66 | 67 | return [self.coreapi_field_to_parameter(field) for field in fields] 68 | 69 | @ignore_assert_decorator 70 | def get_filter_parameters(self, filter_backend): 71 | fields = [] 72 | if hasattr(filter_backend, "get_schema_fields"): 73 | fields = filter_backend.get_schema_fields(self.view) 74 | return [self.coreapi_field_to_parameter(field) for field in fields] 75 | 76 | def coreapi_field_to_parameter(self, field): 77 | """Convert an instance of `coreapi.Field` to a swagger :class:`.Parameter` 78 | object. 79 | 80 | :param coreapi.Field field: 81 | :rtype: openapi.Parameter 82 | """ 83 | location_to_in = { 84 | "query": openapi.IN_QUERY, 85 | "path": openapi.IN_PATH, 86 | "form": openapi.IN_FORM, 87 | "body": openapi.IN_FORM, 88 | } 89 | coreapi_types = { 90 | coreschema.Integer: openapi.TYPE_INTEGER, 91 | coreschema.Number: openapi.TYPE_NUMBER, 92 | coreschema.String: openapi.TYPE_STRING, 93 | coreschema.Boolean: openapi.TYPE_BOOLEAN, 94 | } 95 | 96 | coreschema_attrs = ["format", "pattern", "enum", "min_length", "max_length"] 97 | schema = field.schema 98 | return openapi.Parameter( 99 | name=field.name, 100 | in_=location_to_in[field.location], 101 | required=field.required, 102 | description=force_real_str(schema.description) if schema else None, 103 | type=coreapi_types.get(type(schema), openapi.TYPE_STRING), 104 | **OrderedDict( 105 | (attr, getattr(schema, attr, None)) for attr in coreschema_attrs 106 | ), 107 | ) 108 | 109 | 110 | class DjangoRestResponsePagination(PaginatorInspector): 111 | """Provides response schema pagination wrapping for django-rest-framework' 112 | LimitOffsetPagination, PageNumberPagination and CursorPagination 113 | """ 114 | 115 | def fix_paginated_property(self, key: str, value: dict): 116 | # Need to remove useless params from schema 117 | value.pop("example", None) 118 | if "nullable" in value: 119 | value["x-nullable"] = value.pop("nullable") 120 | if key in {"next", "previous"} and "format" not in value: 121 | value["format"] = "uri" 122 | return openapi.Schema(**value) 123 | 124 | def get_paginated_response(self, paginator, response_schema): 125 | if hasattr(paginator, "get_paginated_response_schema"): 126 | paginator_schema = paginator.get_paginated_response_schema(response_schema) 127 | if paginator_schema["type"] == openapi.TYPE_OBJECT: 128 | properties = { 129 | k: self.fix_paginated_property(k, v) 130 | for k, v in paginator_schema.pop("properties").items() 131 | } 132 | if "required" not in paginator_schema: 133 | paginator_schema.setdefault("required", []) 134 | for prop in ("count", "results"): 135 | if prop in properties: 136 | paginator_schema["required"].append(prop) 137 | return openapi.Schema(**paginator_schema, properties=properties) 138 | else: 139 | return openapi.Schema(**paginator_schema) 140 | 141 | return response_schema 142 | -------------------------------------------------------------------------------- /src/drf_yasg/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/src/drf_yasg/management/__init__.py -------------------------------------------------------------------------------- /src/drf_yasg/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/src/drf_yasg/management/commands/__init__.py -------------------------------------------------------------------------------- /src/drf_yasg/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from .codecs import _OpenAPICodec 4 | from .errors import SwaggerValidationError 5 | 6 | 7 | class SwaggerExceptionMiddleware: 8 | def __init__(self, get_response): 9 | self.get_response = get_response 10 | 11 | def __call__(self, request): 12 | return self.get_response(request) 13 | 14 | def process_exception(self, request, exception): 15 | if isinstance(exception, SwaggerValidationError): 16 | err = {"errors": exception.errors, "message": str(exception)} 17 | codec = exception.source_codec 18 | if isinstance(codec, _OpenAPICodec): 19 | err = codec.encode_error(err) 20 | content_type = codec.media_type 21 | return HttpResponse(err, status=500, content_type=content_type) 22 | 23 | return None # pragma: no cover 24 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/README: -------------------------------------------------------------------------------- 1 | Information about external resources 2 | 3 | The following files are taken from external resources or trees. 4 | 5 | Files: insQ.js 6 | insQ.min.js 7 | License: MIT 8 | Copyright: Zbyszek Tenerowicz 9 | Eryk Napierała 10 | Askar Yusupov 11 | Dan Dascalescu 12 | Source: https://github.com/naugtur/insertionQuery v1.0.3 13 | 14 | Files: immutable.js 15 | immutable.min.js 16 | License: MIT 17 | Copyright: 2014-present, Facebook, Inc 18 | Source: https://github.com/immutable-js/immutable-js/releases/tag/v3.8.2 19 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/insQ.js: -------------------------------------------------------------------------------- 1 | var insertionQ = (function () { 2 | "use strict"; 3 | 4 | var sequence = 100, 5 | isAnimationSupported = false, 6 | animation_string = 'animationName', 7 | keyframeprefix = '', 8 | domPrefixes = 'Webkit Moz O ms Khtml'.split(' '), 9 | pfx = '', 10 | elm = document.createElement('div'), 11 | options = { 12 | strictlyNew: true, 13 | timeout: 20 14 | }; 15 | 16 | if (elm.style.animationName) { 17 | isAnimationSupported = true; 18 | } 19 | 20 | if (isAnimationSupported === false) { 21 | for (var i = 0; i < domPrefixes.length; i++) { 22 | if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { 23 | pfx = domPrefixes[i]; 24 | animation_string = pfx + 'AnimationName'; 25 | keyframeprefix = '-' + pfx.toLowerCase() + '-'; 26 | isAnimationSupported = true; 27 | break; 28 | } 29 | } 30 | } 31 | 32 | 33 | function listen(selector, callback) { 34 | var styleAnimation, animationName = 'insQ_' + (sequence++); 35 | 36 | var eventHandler = function (event) { 37 | if (event.animationName === animationName || event[animation_string] === animationName) { 38 | if (!isTagged(event.target)) { 39 | callback(event.target); 40 | } 41 | } 42 | }; 43 | 44 | styleAnimation = document.createElement('style'); 45 | styleAnimation.innerHTML = '@' + keyframeprefix + 'keyframes ' + animationName + ' { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }' + 46 | "\n" + selector + ' { animation-duration: 0.001s; animation-name: ' + animationName + '; ' + 47 | keyframeprefix + 'animation-duration: 0.001s; ' + keyframeprefix + 'animation-name: ' + animationName + '; ' + 48 | ' } '; 49 | 50 | document.head.appendChild(styleAnimation); 51 | 52 | var bindAnimationLater = setTimeout(function () { 53 | document.addEventListener('animationstart', eventHandler, false); 54 | document.addEventListener('MSAnimationStart', eventHandler, false); 55 | document.addEventListener('webkitAnimationStart', eventHandler, false); 56 | //event support is not consistent with DOM prefixes 57 | }, options.timeout); //starts listening later to skip elements found on startup. this might need tweaking 58 | 59 | return { 60 | destroy: function () { 61 | clearTimeout(bindAnimationLater); 62 | if (styleAnimation) { 63 | document.head.removeChild(styleAnimation); 64 | styleAnimation = null; 65 | } 66 | document.removeEventListener('animationstart', eventHandler); 67 | document.removeEventListener('MSAnimationStart', eventHandler); 68 | document.removeEventListener('webkitAnimationStart', eventHandler); 69 | } 70 | }; 71 | } 72 | 73 | 74 | function tag(el) { 75 | el.QinsQ = true; //bug in V8 causes memory leaks when weird characters are used as field names. I don't want to risk leaking DOM trees so the key is not '-+-' anymore 76 | } 77 | 78 | function isTagged(el) { 79 | return (options.strictlyNew && (el.QinsQ === true)); 80 | } 81 | 82 | function topmostUntaggedParent(el) { 83 | if (isTagged(el.parentNode)) { 84 | return el; 85 | } else { 86 | return topmostUntaggedParent(el.parentNode); 87 | } 88 | } 89 | 90 | function tagAll(e) { 91 | tag(e); 92 | e = e.firstChild; 93 | for (; e; e = e.nextSibling) { 94 | if (e !== undefined && e.nodeType === 1) { 95 | tagAll(e); 96 | } 97 | } 98 | } 99 | 100 | //aggregates multiple insertion events into a common parent 101 | function catchInsertions(selector, callback) { 102 | var insertions = []; 103 | //throttle summary 104 | var sumUp = (function () { 105 | var to; 106 | return function () { 107 | clearTimeout(to); 108 | to = setTimeout(function () { 109 | insertions.forEach(tagAll); 110 | callback(insertions); 111 | insertions = []; 112 | }, 10); 113 | }; 114 | })(); 115 | 116 | return listen(selector, function (el) { 117 | if (isTagged(el)) { 118 | return; 119 | } 120 | tag(el); 121 | var myparent = topmostUntaggedParent(el); 122 | if (insertions.indexOf(myparent) < 0) { 123 | insertions.push(myparent); 124 | } 125 | sumUp(); 126 | }); 127 | } 128 | 129 | //insQ function 130 | var exports = function (selector) { 131 | if (isAnimationSupported && selector.match(/[^{}]/)) { 132 | 133 | if (options.strictlyNew) { 134 | tagAll(document.body); //prevents from catching things on show 135 | } 136 | return { 137 | every: function (callback) { 138 | return listen(selector, callback); 139 | }, 140 | summary: function (callback) { 141 | return catchInsertions(selector, callback); 142 | } 143 | }; 144 | } else { 145 | return false; 146 | } 147 | }; 148 | 149 | //allows overriding defaults 150 | exports.config = function (opt) { 151 | for (var o in opt) { 152 | if (opt.hasOwnProperty(o)) { 153 | options[o] = opt[o]; 154 | } 155 | } 156 | }; 157 | 158 | return exports; 159 | })(); 160 | 161 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 162 | module.exports = insertionQ; 163 | } 164 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/insQ.min.js: -------------------------------------------------------------------------------- 1 | // insertion-query v1.0.3 (2016-01-20) 2 | // license:MIT 3 | // Zbyszek Tenerowicz (http://naugtur.pl/) 4 | var insertionQ=function(){"use strict";function a(a,b){var d,e="insQ_"+g++,f=function(a){(a.animationName===e||a[i]===e)&&(c(a.target)||b(a.target))};d=document.createElement("style"),d.innerHTML="@"+j+"keyframes "+e+" { from { outline: 1px solid transparent } to { outline: 0px solid transparent } }\n"+a+" { animation-duration: 0.001s; animation-name: "+e+"; "+j+"animation-duration: 0.001s; "+j+"animation-name: "+e+"; } ",document.head.appendChild(d);var h=setTimeout(function(){document.addEventListener("animationstart",f,!1),document.addEventListener("MSAnimationStart",f,!1),document.addEventListener("webkitAnimationStart",f,!1)},n.timeout);return{destroy:function(){clearTimeout(h),d&&(document.head.removeChild(d),d=null),document.removeEventListener("animationstart",f),document.removeEventListener("MSAnimationStart",f),document.removeEventListener("webkitAnimationStart",f)}}}function b(a){a.QinsQ=!0}function c(a){return n.strictlyNew&&a.QinsQ===!0}function d(a){return c(a.parentNode)?a:d(a.parentNode)}function e(a){for(b(a),a=a.firstChild;a;a=a.nextSibling)void 0!==a&&1===a.nodeType&&e(a)}function f(f,g){var h=[],i=function(){var a;return function(){clearTimeout(a),a=setTimeout(function(){h.forEach(e),g(h),h=[]},10)}}();return a(f,function(a){if(!c(a)){b(a);var e=d(a);h.indexOf(e)<0&&h.push(e),i()}})}var g=100,h=!1,i="animationName",j="",k="Webkit Moz O ms Khtml".split(" "),l="",m=document.createElement("div"),n={strictlyNew:!0,timeout:20};if(m.style.animationName&&(h=!0),h===!1)for(var o=0;o 4 | // This file is licensed under the BSD 3-Clause License. 5 | // License text available at https://opensource.org/licenses/BSD-3-Clause 6 | 7 | "use strict"; 8 | 9 | var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname; 10 | var specURL = currentPath + '?format=openapi'; 11 | var redoc = document.createElement("redoc"); 12 | 13 | var redocSettings = JSON.parse(document.getElementById('redoc-settings').innerHTML); 14 | if (redocSettings.url) { 15 | specURL = redocSettings.url; 16 | } 17 | delete redocSettings.url; 18 | if (redocSettings.fetchSchemaWithQuery) { 19 | var query = new URLSearchParams(window.location.search || '').entries(); 20 | var url = specURL.split('?'); 21 | var usp = new URLSearchParams(url[1] || ''); 22 | for (var it = query.next(); !it.done; it = query.next()) { 23 | usp.set(it.value[0], it.value[1]); 24 | } 25 | url[1] = usp.toString(); 26 | specURL = url[1] ? url.join('?') : url[0]; 27 | } 28 | delete redocSettings.fetchSchemaWithQuery; 29 | 30 | redoc.setAttribute("spec-url", specURL); 31 | 32 | function camelToKebab(str) { 33 | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 34 | } 35 | 36 | for (var p in redocSettings) { 37 | if (redocSettings.hasOwnProperty(p)) { 38 | if (redocSettings[p] !== null && redocSettings[p] !== undefined && redocSettings[p] !== false) { 39 | redoc.setAttribute(camelToKebab(p), redocSettings[p].toString()); 40 | } 41 | } 42 | } 43 | 44 | document.body.replaceChild(redoc, document.getElementById('redoc-placeholder')); 45 | 46 | function hideEmptyVersion() { 47 | // 'span.api-info-version' is for redoc 1.x, 'div.api-info span' is for redoc 2-alpha 48 | var apiVersion = document.querySelector('span.api-info-version') || document.querySelector('div.api-info span'); 49 | if (!apiVersion) { 50 | console.log("WARNING: could not find API versionString element (span.api-info-version)"); 51 | return; 52 | } 53 | 54 | var versionString = apiVersion.innerText; 55 | if (versionString) { 56 | // trim spaces and surrounding () 57 | versionString = versionString.replace(/ /g, ''); 58 | versionString = versionString.replace(/(^\()|(\)$)/g, ''); 59 | } 60 | 61 | if (!versionString) { 62 | // hide version element if empty 63 | apiVersion.classList.add("hidden"); 64 | } 65 | } 66 | 67 | if (document.querySelector('span.api-info-version') || document.querySelector('div.api-info span')) { 68 | hideEmptyVersion(); 69 | } 70 | else { 71 | insertionQ('span.api-info-version').every(hideEmptyVersion); 72 | insertionQ('div.api-info span').every(hideEmptyVersion); 73 | } 74 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/redoc-old/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Rebilly, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/redoc-old/redoc.min.js.map: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/redoc/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Rebilly, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/redoc/redoc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/src/drf_yasg/static/drf-yasg/redoc/redoc-logo.png -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | body.swagger-body { 19 | background: #fafafa; 20 | } 21 | 22 | .hidden { 23 | display: none; 24 | } 25 | 26 | #django-session-auth > div { 27 | display: inline-block; 28 | } 29 | 30 | #django-session-auth .btn.authorize { 31 | padding: 10px 23px; 32 | } 33 | 34 | #django-session-auth .btn.authorize a { 35 | color: #49cc90; 36 | text-decoration: none; 37 | } 38 | 39 | #django-session-auth .hello { 40 | margin-right: 5px; 41 | } 42 | 43 | #django-session-auth .hello .django-session { 44 | font-weight: bold; 45 | } 46 | 47 | .label { 48 | display: inline; 49 | padding: .2em .6em .3em; 50 | font-weight: 700; 51 | line-height: 1; 52 | color: #fff; 53 | text-align: center; 54 | white-space: nowrap; 55 | vertical-align: baseline; 56 | border-radius: .25em; 57 | } 58 | 59 | .label-primary { 60 | background-color: #337ab7; 61 | } 62 | 63 | .divider { 64 | margin-right: 8px; 65 | background: #16222c44; 66 | width: 2px; 67 | } 68 | 69 | svg.swagger-defs { 70 | position: absolute; 71 | width: 0; 72 | height: 0; 73 | } 74 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/NOTICE: -------------------------------------------------------------------------------- 1 | swagger-ui 2 | Copyright 2020-2021 SmartBear Software Inc. 3 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/absolute-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * getAbsoluteFSPath 3 | * @return {string} When run in NodeJS env, returns the absolute path to the current directory 4 | * When run outside of NodeJS, will return an error message 5 | */ 6 | const getAbsoluteFSPath = function () { 7 | // detect whether we are running in a browser or nodejs 8 | if (typeof module !== "undefined" && module.exports) { 9 | return require("path").resolve(__dirname) 10 | } 11 | throw new Error('getAbsoluteFSPath can only be called within a Nodejs environment'); 12 | } 13 | 14 | module.exports = getAbsoluteFSPath 15 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/src/drf_yasg/static/drf-yasg/swagger-ui-dist/favicon-32x32.png -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/index.js: -------------------------------------------------------------------------------- 1 | try { 2 | module.exports.SwaggerUIBundle = require("./swagger-ui-bundle.js") 3 | module.exports.SwaggerUIStandalonePreset = require("./swagger-ui-standalone-preset.js") 4 | } catch(e) { 5 | // swallow the error if there's a problem loading the assets. 6 | // allows this module to support providing the assets for browserish contexts, 7 | // without exploding in a Node context. 8 | // 9 | // see https://github.com/swagger-api/swagger-ui/issues/3291#issuecomment-311195388 10 | // for more information. 11 | } 12 | 13 | // `absolutePath` and `getAbsoluteFSPath` are both here because at one point, 14 | // we documented having one and actually implemented the other. 15 | // They were both retained so we don't break anyone's code. 16 | module.exports.absolutePath = require("./absolute-path.js") 17 | module.exports.getAbsoluteFSPath = require("./absolute-path.js") 18 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/drf_yasg/static/drf-yasg/swagger-ui-dist/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "https://petstore.swagger.io/v2/swagger.json", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | layout: "StandaloneLayout" 17 | }); 18 | 19 | // 20 | }; 21 | -------------------------------------------------------------------------------- /src/drf_yasg/templates/drf-yasg/redoc-old.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/drf_yasg/templates/drf-yasg/redoc.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{{ title }}{% endblock %} 8 | 9 | {% block extra_head %} 10 | {# -- Add any extra HTML heads tags here - except scripts and styles -- #} 11 | {% endblock %} 12 | 13 | {% block favicon %} 14 | {# -- Maybe replace the favicon -- #} 15 | 16 | {% endblock %} 17 | 18 | {% block main_styles %} 19 | 20 | {% endblock %} 21 | {% block extra_styles %} 22 | {# -- Add any additional CSS scripts here -- #} 23 | {% endblock %} 24 | 25 | 26 | 27 | {% block extra_body %} 28 | {# -- Add any header/body markup here (rendered BEFORE the swagger-ui/redoc element) -- #} 29 | {% endblock %} 30 | 31 |
32 | 33 | {% block footer %} 34 | {# -- Add any footer markup here (rendered AFTER the swagger-ui/redoc element) -- #} 35 | {% endblock %} 36 | 37 | 38 | 39 | {% block main_scripts %} 40 | 41 | 42 | 43 | {% endblock %} 44 | {% block extra_scripts %} 45 | {# -- Add any additional scripts here -- #} 46 | {% endblock %} 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/drf_yasg/templates/drf-yasg/swagger-ui.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}{{ title }}{% endblock %} 7 | 8 | {% block extra_head %} 9 | {# -- Add any extra HTML heads tags here - except scripts and styles -- #} 10 | {% endblock %} 11 | 12 | {% block favicon %} 13 | {# -- Maybe replace the favicon -- #} 14 | 15 | {% endblock %} 16 | 17 | {% block main_styles %} 18 | 19 | 20 | {% endblock %} 21 | {% block extra_styles %} 22 | {# -- Add any additional CSS scripts here -- #} 23 | {% endblock %} 24 | 25 | 26 | 27 | 28 | {% block extra_body %} 29 | {# -- Add any header/body markup here (rendered BEFORE the swagger-ui/redoc element) -- #} 30 | {% endblock %} 31 | 32 |
33 | 34 | {% block footer %} 35 | {# -- Add any footer markup here (rendered AFTER the swagger-ui/redoc element) -- #} 36 | {% endblock %} 37 | 38 | 39 | 40 | 41 | {% block main_scripts %} 42 | 43 | 44 | 45 | 46 | 47 | {% endblock %} 48 | {% block extra_scripts %} 49 | {# -- Add any additional scripts here -- #} 50 | {% endblock %} 51 | 52 | 53 | 54 | {% if USE_SESSION_AUTH %} 55 | 87 | {% endif %} 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /testproj/articles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/articles/__init__.py -------------------------------------------------------------------------------- /testproj/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-03-18 18:32 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Article', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)), 23 | ('body', models.TextField(help_text='article model help_text', max_length=5000)), 24 | ('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)), 25 | ('date_created', models.DateTimeField(auto_now_add=True)), 26 | ('date_modified', models.DateTimeField(auto_now=True)), 27 | ('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)), 28 | ('cover', models.ImageField(blank=True, upload_to='article/original/')), 29 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name='ArticleGroup', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 37 | ('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)), 38 | ('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)), 39 | ], 40 | ), 41 | migrations.AddField( 42 | model_name='article', 43 | name='group', 44 | field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_main', to='articles.ArticleGroup'), 45 | ), 46 | migrations.AddField( 47 | model_name='article', 48 | name='original_group', 49 | field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_original', to='articles.ArticleGroup'), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /testproj/articles/migrations/0002_article_read_only_nullable.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-03-02 03:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('articles', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='article', 15 | name='read_only_nullable', 16 | field=models.CharField(blank=True, max_length=20, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /testproj/articles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/articles/migrations/__init__.py -------------------------------------------------------------------------------- /testproj/articles/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | 5 | 6 | class Article(models.Model): 7 | title = models.CharField( 8 | help_text="title model help_text", max_length=255, blank=False, unique=True 9 | ) 10 | body = models.TextField( 11 | help_text="article model help_text", max_length=5000, blank=False 12 | ) 13 | slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True) 14 | date_created = models.DateTimeField(auto_now_add=True) 15 | date_modified = models.DateTimeField(auto_now=True) 16 | author = models.ForeignKey( 17 | "auth.User", related_name="articles", on_delete=models.CASCADE 18 | ) 19 | article_type = models.PositiveSmallIntegerField( 20 | help_text=( 21 | "IntegerField declared on model with choices=(...) and exposed via " 22 | "ModelSerializer" 23 | ), 24 | choices=((1, "first"), (2, "second"), (3, "third"), (7, "seven"), (8, "eight")), 25 | null=True, 26 | ) 27 | 28 | cover = models.ImageField(upload_to="article/original/", blank=True) 29 | group = models.ForeignKey( 30 | "ArticleGroup", 31 | related_name="articles_as_main", 32 | blank=True, 33 | default=None, 34 | on_delete=models.PROTECT, 35 | ) 36 | original_group = models.ForeignKey( 37 | "ArticleGroup", 38 | related_name="articles_as_original", 39 | blank=True, 40 | default=None, 41 | on_delete=models.PROTECT, 42 | ) 43 | read_only_nullable = models.CharField(max_length=20, null=True, blank=True) 44 | 45 | 46 | class ArticleGroup(models.Model): 47 | uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 48 | 49 | title = models.CharField( 50 | help_text="title model help_text", max_length=255, blank=False, unique=True 51 | ) 52 | slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True) 53 | -------------------------------------------------------------------------------- /testproj/articles/serializers.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from rest_framework import serializers 3 | 4 | from articles.models import Article, ArticleGroup 5 | 6 | 7 | class ArticleSerializer(serializers.ModelSerializer): 8 | references = serializers.DictField( 9 | help_text=_("this is a really bad example"), 10 | child=serializers.URLField( 11 | help_text="but i needed to test these 2 fields somehow" 12 | ), 13 | read_only=True, 14 | ) 15 | uuid = serializers.UUIDField( 16 | help_text="should articles have UUIDs?", read_only=True 17 | ) 18 | cover_name = serializers.FileField(use_url=False, source="cover", required=True) 19 | group = serializers.SlugRelatedField( 20 | slug_field="uuid", queryset=ArticleGroup.objects.all() 21 | ) 22 | original_group = serializers.SlugRelatedField(slug_field="uuid", read_only=True) 23 | 24 | class Meta: 25 | model = Article 26 | fields = ( 27 | "title", 28 | "author", 29 | "body", 30 | "slug", 31 | "date_created", 32 | "date_modified", 33 | "read_only_nullable", 34 | "references", 35 | "uuid", 36 | "cover", 37 | "cover_name", 38 | "article_type", 39 | "group", 40 | "original_group", 41 | ) 42 | read_only_fields = ( 43 | "date_created", 44 | "date_modified", 45 | "references", 46 | "uuid", 47 | "cover_name", 48 | "read_only_nullable", 49 | ) 50 | lookup_field = "slug" 51 | extra_kwargs = { 52 | "body": {"help_text": "body serializer help_text"}, 53 | "author": { 54 | "default": serializers.CurrentUserDefault(), 55 | "help_text": _( 56 | "The ID of the user that created this article; if none is " 57 | "provided, defaults to the currently logged in user." 58 | ), 59 | }, 60 | "read_only_nullable": {"allow_null": True}, 61 | } 62 | 63 | 64 | class ImageUploadSerializer(serializers.Serializer): 65 | image_id = serializers.UUIDField(read_only=True) 66 | what_am_i_doing = serializers.RegexField( 67 | regex=r"^69$", help_text="test", default="69", allow_null=True 68 | ) 69 | image_styles = serializers.ListSerializer( 70 | child=serializers.ChoiceField(choices=["wide", "tall", "thumb", "social"]), 71 | help_text="Parameter with Items", 72 | ) 73 | upload = serializers.ImageField(help_text="image serializer help_text") 74 | -------------------------------------------------------------------------------- /testproj/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework.routers import SimpleRouter 3 | 4 | from articles import views 5 | 6 | router = SimpleRouter() 7 | router.register("", views.ArticleViewSet) 8 | 9 | urlpatterns = [ 10 | path("", include(router.urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /testproj/articles/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils.decorators import method_decorator 4 | from django_filters.rest_framework import DjangoFilterBackend 5 | from rest_framework import viewsets 6 | from rest_framework.filters import OrderingFilter 7 | from rest_framework.pagination import LimitOffsetPagination 8 | from rest_framework.parsers import FileUploadParser, MultiPartParser 9 | from rest_framework.response import Response 10 | 11 | from articles import serializers 12 | from articles.models import Article 13 | from drf_yasg import openapi 14 | from drf_yasg.app_settings import swagger_settings 15 | from drf_yasg.inspectors import ( 16 | DrfAPICompatInspector, 17 | FieldInspector, 18 | NotHandled, 19 | SwaggerAutoSchema, 20 | ) 21 | from drf_yasg.utils import no_body, swagger_auto_schema 22 | 23 | 24 | class DjangoFilterDescriptionInspector(DrfAPICompatInspector): 25 | def get_filter_parameters(self, filter_backend): 26 | if isinstance(filter_backend, DjangoFilterBackend): 27 | result = super( 28 | DjangoFilterDescriptionInspector, self 29 | ).get_filter_parameters(filter_backend) 30 | for param in result: 31 | if ( 32 | not param.get("description", "") 33 | or param.get("description") == param.name 34 | ): 35 | param.description = ( 36 | "Filter the returned list by {field_name}".format( 37 | field_name=param.name 38 | ) 39 | ) 40 | 41 | return result 42 | 43 | return NotHandled 44 | 45 | 46 | class NoSchemaTitleInspector(FieldInspector): 47 | def process_result(self, result, method_name, obj, **kwargs): 48 | # remove the `title` attribute of all Schema objects 49 | if isinstance(result, openapi.Schema.OR_REF): 50 | # traverse any references and alter the Schema object in place 51 | schema = openapi.resolve_ref(result, self.components) 52 | schema.pop("title", None) 53 | 54 | # no ``return schema`` here, because it would mean we always generate 55 | # an inline `object` instead of a definition reference 56 | 57 | # return back the same object that we got - i.e. a reference if we got a 58 | # reference 59 | return result 60 | 61 | 62 | class NoTitleAutoSchema(SwaggerAutoSchema): 63 | field_inspectors = [ 64 | NoSchemaTitleInspector 65 | ] + swagger_settings.DEFAULT_FIELD_INSPECTORS 66 | 67 | 68 | class NoPagingAutoSchema(NoTitleAutoSchema): 69 | def should_page(self): 70 | return False 71 | 72 | 73 | class ArticlePagination(LimitOffsetPagination): 74 | default_limit = 5 75 | max_limit = 25 76 | 77 | 78 | @method_decorator( 79 | name="list", 80 | decorator=swagger_auto_schema( 81 | operation_description=( 82 | "description from swagger_auto_schema via method_decorator" 83 | ), 84 | filter_inspectors=[DjangoFilterDescriptionInspector], 85 | ), 86 | ) 87 | class ArticleViewSet(viewsets.ModelViewSet): 88 | """ 89 | ArticleViewSet class docstring 90 | 91 | retrieve: 92 | retrieve class docstring 93 | 94 | destroy: 95 | destroy class docstring 96 | 97 | partial_update: 98 | partial_update class docstring 99 | """ 100 | 101 | queryset = Article.objects.all() 102 | lookup_field = "slug" 103 | lookup_value_regex = r"[a-z0-9]+(?:-[a-z0-9]+)" 104 | serializer_class = serializers.ArticleSerializer 105 | 106 | pagination_class = ArticlePagination 107 | filter_backends = (DjangoFilterBackend, OrderingFilter) 108 | filterset_fields = ("title",) 109 | # django-filter 1.1 compatibility; was renamed to filterset_fields in 2.0 110 | # TODO: remove when dropping support for Django 1.11 111 | filter_fields = filterset_fields 112 | ordering_fields = ("date_modified", "date_created") 113 | ordering = ("date_created",) 114 | 115 | swagger_schema = NoTitleAutoSchema 116 | 117 | from rest_framework.decorators import action 118 | 119 | @swagger_auto_schema( 120 | auto_schema=NoPagingAutoSchema, 121 | filter_inspectors=[DjangoFilterDescriptionInspector], 122 | ) 123 | @action(detail=False, methods=["get"]) 124 | def today(self, request): 125 | today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min) 126 | today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max) 127 | articles = ( 128 | self.get_queryset().filter(date_created__range=(today_min, today_max)).all() 129 | ) 130 | serializer = self.serializer_class(articles, many=True) 131 | return Response(serializer.data) 132 | 133 | @swagger_auto_schema( 134 | method="get", operation_description="image GET description override" 135 | ) 136 | @swagger_auto_schema(method="post", request_body=serializers.ImageUploadSerializer) 137 | @swagger_auto_schema( 138 | method="delete", 139 | manual_parameters=[ 140 | openapi.Parameter( 141 | name="delete_form_param", 142 | in_=openapi.IN_FORM, 143 | type=openapi.TYPE_INTEGER, 144 | description="this should not crash (form parameter on DELETE method)", 145 | ) 146 | ], 147 | ) 148 | @action( 149 | detail=True, 150 | methods=["get", "post", "delete"], 151 | parser_classes=(MultiPartParser, FileUploadParser), 152 | ) 153 | def image(self, request, slug=None): 154 | """ 155 | image method docstring 156 | """ 157 | pass 158 | 159 | @swagger_auto_schema(request_body=no_body, operation_id="no_body_test") 160 | def update(self, request, *args, **kwargs): 161 | """update method docstring""" 162 | return super(ArticleViewSet, self).update(request, *args, **kwargs) 163 | 164 | @swagger_auto_schema( 165 | operation_description="partial_update description override", 166 | responses={404: "slug not found"}, 167 | operation_summary="partial_update summary", 168 | deprecated=True, 169 | ) 170 | def partial_update(self, request, *args, **kwargs): 171 | """partial_update method docstring""" 172 | return super(ArticleViewSet, self).partial_update(request, *args, **kwargs) 173 | 174 | def destroy(self, request, *args, **kwargs): 175 | """destroy method docstring""" 176 | return super(ArticleViewSet, self).destroy(request, *args, **kwargs) 177 | -------------------------------------------------------------------------------- /testproj/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | try: 11 | import django # noqa: F401 12 | except ImportError: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) 18 | raise 19 | execute_from_command_line(sys.argv) 20 | -------------------------------------------------------------------------------- /testproj/people/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/people/__init__.py -------------------------------------------------------------------------------- /testproj/people/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PeopleConfig(AppConfig): 5 | name = "people" 6 | -------------------------------------------------------------------------------- /testproj/people/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-03-18 18:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Identity', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('firstName', models.CharField(max_length=30, null=True)), 20 | ('lastName', models.CharField(max_length=30, null=True)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Person', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('identity', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='person', to='people.Identity')), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /testproj/people/migrations/0002_rename_identity_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-06 13:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('people', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='identity', 15 | name='lastName', 16 | field=models.CharField(help_text="Here's some HTML!", max_length=30, null=True), 17 | ), 18 | migrations.RenameField( 19 | model_name='identity', 20 | old_name='firstName', 21 | new_name='first_name', 22 | ), 23 | migrations.RenameField( 24 | model_name='identity', 25 | old_name='lastName', 26 | new_name='last_name', 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /testproj/people/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/people/migrations/__init__.py -------------------------------------------------------------------------------- /testproj/people/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.safestring import mark_safe 3 | 4 | 5 | class Identity(models.Model): 6 | first_name = models.CharField(max_length=30, null=True) 7 | last_name = models.CharField( 8 | max_length=30, 9 | null=True, 10 | help_text=mark_safe("Here's some HTML!"), 11 | ) 12 | 13 | 14 | class Person(models.Model): 15 | identity = models.OneToOneField( 16 | Identity, related_name="person", on_delete=models.PROTECT 17 | ) 18 | -------------------------------------------------------------------------------- /testproj/people/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Identity, Person 4 | 5 | 6 | class IdentitySerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Identity 9 | fields = "__all__" 10 | 11 | 12 | class PersonSerializer(serializers.ModelSerializer): 13 | identity = IdentitySerializer() 14 | 15 | class Meta: 16 | model = Person 17 | fields = "__all__" 18 | 19 | def create(self, validated_data): 20 | identity = Identity(**validated_data["identity"]) 21 | identity.save() 22 | validated_data["identity"] = identity 23 | return super().create(validated_data) 24 | -------------------------------------------------------------------------------- /testproj/people/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import IdentityViewSet, PersonViewSet 4 | 5 | person_list = PersonViewSet.as_view({"get": "list", "post": "create"}) 6 | person_detail = PersonViewSet.as_view( 7 | {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} 8 | ) 9 | 10 | identity_detail = IdentityViewSet.as_view( 11 | { 12 | "get": "retrieve", 13 | "patch": "partial_update", 14 | } 15 | ) 16 | 17 | urlpatterns = ( 18 | path("", person_list, name="people-list"), 19 | path("", person_detail, name="person-detail"), 20 | path("/identity", identity_detail, name="person-identity"), 21 | ) 22 | -------------------------------------------------------------------------------- /testproj/people/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.pagination import BasePagination 3 | 4 | from .models import Identity, Person 5 | from .serializers import IdentitySerializer, PersonSerializer 6 | 7 | 8 | class UnknownPagination(BasePagination): 9 | paginator_query_args = ["unknown_paginator"] 10 | 11 | 12 | class PersonViewSet(viewsets.ModelViewSet): 13 | model = Person 14 | queryset = Person.objects 15 | serializer_class = PersonSerializer 16 | pagination_class = UnknownPagination 17 | 18 | 19 | class IdentityViewSet(viewsets.ModelViewSet): 20 | model = Identity 21 | queryset = Identity.objects 22 | serializer_class = IdentitySerializer 23 | -------------------------------------------------------------------------------- /testproj/requirements.txt: -------------------------------------------------------------------------------- 1 | ..[validation] 2 | -r ../requirements/testproj.txt 3 | -------------------------------------------------------------------------------- /testproj/snippets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/snippets/__init__.py -------------------------------------------------------------------------------- /testproj/snippets/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /testproj/snippets/migrations/0003_snippetviewer.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-16 14:06 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('snippets', '0002_auto_20181219_1016'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='SnippetViewer', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('snippet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewers', to='snippets.Snippet')), 21 | ('viewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippet_views', to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /testproj/snippets/migrations/0004_auto_20190613_0154.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-06-12 22:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('snippets', '0003_snippetviewer'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='snippet', 15 | name='language', 16 | field=models.CharField(choices=[('cpp', 'cpp'), ('js', 'js'), ('python', 'python')], default='python', max_length=100), 17 | ), 18 | migrations.AlterField( 19 | model_name='snippet', 20 | name='style', 21 | field=models.CharField(choices=[('monokai', 'monokai'), ('solarized-dark', 'solarized-dark'), ('vim', 'vim')], default='solarized-dark', max_length=100), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /testproj/snippets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/snippets/migrations/__init__.py -------------------------------------------------------------------------------- /testproj/snippets/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | LANGUAGE_CHOICES = sorted((item, item) for item in ("cpp", "python", "js")) 4 | STYLE_CHOICES = sorted((item, item) for item in ("solarized-dark", "monokai", "vim")) 5 | 6 | 7 | class Snippet(models.Model): 8 | created = models.DateTimeField(auto_now_add=True) 9 | owner = models.ForeignKey( 10 | "auth.User", related_name="snippets", on_delete=models.CASCADE 11 | ) 12 | title = models.CharField(max_length=100, blank=True, default="") 13 | code = models.TextField(help_text="code model help text") 14 | linenos = models.BooleanField(default=False) 15 | language = models.CharField( 16 | choices=LANGUAGE_CHOICES, default="python", max_length=100 17 | ) 18 | style = models.CharField( 19 | choices=STYLE_CHOICES, default="solarized-dark", max_length=100 20 | ) 21 | 22 | class Meta: 23 | ordering = ("created",) 24 | 25 | @property 26 | def owner_snippets(self): 27 | return Snippet._default_manager.filter(owner=self.owner) 28 | 29 | @property 30 | def nullable_secondary_language(self): 31 | return None 32 | 33 | 34 | class SnippetViewer(models.Model): 35 | snippet = models.ForeignKey( 36 | Snippet, on_delete=models.CASCADE, related_name="viewers" 37 | ) 38 | viewer = models.ForeignKey( 39 | "auth.User", related_name="snippet_views", on_delete=models.CASCADE 40 | ) 41 | -------------------------------------------------------------------------------- /testproj/snippets/serializers.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.contrib.auth import get_user_model 4 | from django.core.validators import MaxLengthValidator, MinValueValidator 5 | from rest_framework import serializers 6 | 7 | from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer 8 | 9 | 10 | class LanguageSerializer(serializers.Serializer): 11 | name = serializers.ChoiceField( 12 | choices=LANGUAGE_CHOICES, 13 | default="python", 14 | help_text="The name of the programming language", 15 | ) 16 | read_only_nullable = serializers.CharField(read_only=True, allow_null=True) 17 | 18 | class Meta: 19 | ref_name = None 20 | 21 | 22 | class ExampleProjectSerializer(serializers.Serializer): 23 | project_name = serializers.CharField( 24 | label="project name custom title", help_text="Name of the project" 25 | ) 26 | github_repo = serializers.CharField( 27 | required=True, help_text="Github repository of the project" 28 | ) 29 | 30 | class Meta: 31 | ref_name = "Project" 32 | 33 | 34 | class UnixTimestampField(serializers.DateTimeField): 35 | def to_representation(self, value): 36 | """Return epoch time for a datetime object or ``None``""" 37 | from django.utils.dateformat import format 38 | 39 | try: 40 | return int(format(value, "U")) 41 | except (AttributeError, TypeError): 42 | return None 43 | 44 | def to_internal_value(self, value): 45 | import datetime 46 | 47 | return datetime.datetime.fromtimestamp(int(value)) 48 | 49 | class Meta: 50 | swagger_schema_fields = { 51 | "format": "integer", 52 | "title": "Client date time suu", 53 | "description": "Date time in unix timestamp format", 54 | } 55 | 56 | 57 | class SnippetSerializer(serializers.Serializer): 58 | """SnippetSerializer classdoc 59 | 60 | create: docstring for create from serializer classdoc 61 | """ 62 | 63 | id = serializers.IntegerField(read_only=True, help_text="id serializer help text") 64 | created = UnixTimestampField(read_only=True) 65 | owner = serializers.PrimaryKeyRelatedField( 66 | queryset=get_user_model().objects.all(), 67 | default=serializers.CurrentUserDefault(), 68 | help_text="The ID of the user that created this snippet; if none is provided, " 69 | "defaults to the currently logged in user.", 70 | ) 71 | owner_as_string = serializers.PrimaryKeyRelatedField( 72 | help_text="The ID of the user that created this snippet.", 73 | pk_field=serializers.CharField(help_text="this help text should not show up"), 74 | read_only=True, 75 | source="owner", 76 | ) 77 | title = serializers.CharField(required=False, allow_blank=True, max_length=100) 78 | code = serializers.CharField(style={"base_template": "textarea.html"}) 79 | tags = serializers.ListField( 80 | child=serializers.CharField(min_length=2), min_length=3, max_length=15 81 | ) 82 | linenos = serializers.BooleanField(required=False) 83 | language = LanguageSerializer(help_text="Sample help text for language") 84 | styles = serializers.MultipleChoiceField( 85 | choices=STYLE_CHOICES, default=["solarized-dark"] 86 | ) 87 | lines = serializers.ListField( 88 | child=serializers.IntegerField(), 89 | allow_empty=True, 90 | allow_null=True, 91 | required=False, 92 | ) 93 | example_projects = serializers.ListSerializer( 94 | child=ExampleProjectSerializer(), 95 | read_only=True, 96 | validators=[MaxLengthValidator(100)], 97 | ) 98 | difficulty_factor = serializers.FloatField( 99 | help_text="this is here just to test FloatField", 100 | read_only=True, 101 | default=lambda: 6.9, 102 | ) 103 | rate_as_string = serializers.DecimalField( 104 | max_digits=6, 105 | decimal_places=3, 106 | default=Decimal("0.0"), 107 | validators=[MinValueValidator(Decimal("0.0"))], 108 | ) 109 | rate = serializers.DecimalField( 110 | max_digits=6, 111 | decimal_places=3, 112 | default=Decimal("0.0"), 113 | coerce_to_string=False, 114 | validators=[MinValueValidator(Decimal("0.0"))], 115 | ) 116 | 117 | nullable_secondary_language = LanguageSerializer(allow_null=True) 118 | 119 | def create(self, validated_data): 120 | """ 121 | Create and return a new `Snippet` instance, given the validated data. 122 | """ 123 | del validated_data["styles"] 124 | del validated_data["lines"] 125 | del validated_data["difficulty_factor"] 126 | return Snippet.objects.create(**validated_data) 127 | 128 | def update(self, instance, validated_data): 129 | """ 130 | Update and return an existing `Snippet` instance, given the validated data. 131 | """ 132 | instance.title = validated_data.get("title", instance.title) 133 | instance.code = validated_data.get("code", instance.code) 134 | instance.linenos = validated_data.get("linenos", instance.linenos) 135 | instance.language = validated_data.get("language", instance.language) 136 | instance.style = validated_data.get("style", instance.style) 137 | instance.save() 138 | return instance 139 | 140 | 141 | class SnippetViewerSerializer(serializers.ModelSerializer): 142 | class Meta: 143 | model = SnippetViewer 144 | fields = "__all__" 145 | -------------------------------------------------------------------------------- /testproj/snippets/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("", views.SnippetList.as_view()), 7 | path("/", views.SnippetDetail.as_view()), 8 | path("views//", views.SnippetViewerList.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /testproj/snippets/views.py: -------------------------------------------------------------------------------- 1 | from djangorestframework_camel_case.parser import CamelCaseJSONParser 2 | from djangorestframework_camel_case.render import CamelCaseJSONRenderer 3 | from inflection import camelize 4 | from rest_framework import generics, status 5 | from rest_framework.generics import get_object_or_404 6 | from rest_framework.pagination import PageNumberPagination 7 | from rest_framework.parsers import FileUploadParser, FormParser 8 | 9 | from drf_yasg import openapi 10 | from drf_yasg.inspectors import SwaggerAutoSchema 11 | from drf_yasg.utils import swagger_auto_schema 12 | from snippets.models import Snippet, SnippetViewer 13 | from snippets.serializers import SnippetSerializer, SnippetViewerSerializer 14 | 15 | 16 | class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema): 17 | def get_operation_id(self, operation_keys): 18 | operation_id = super(CamelCaseOperationIDAutoSchema, self).get_operation_id( 19 | operation_keys 20 | ) 21 | return camelize(operation_id, uppercase_first_letter=False) 22 | 23 | 24 | class SnippetList(generics.ListCreateAPIView): 25 | """SnippetList classdoc""" 26 | 27 | queryset = Snippet.objects.all() 28 | serializer_class = SnippetSerializer 29 | 30 | parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser) 31 | renderer_classes = (CamelCaseJSONRenderer,) 32 | swagger_schema = CamelCaseOperationIDAutoSchema 33 | 34 | def perform_create(self, serializer): 35 | serializer.save(owner=self.request.user) 36 | 37 | def post(self, request, *args, **kwargs): 38 | """post method docstring""" 39 | return super(SnippetList, self).post(request, *args, **kwargs) 40 | 41 | @swagger_auto_schema( 42 | operation_id="snippets_delete_bulk", 43 | request_body=openapi.Schema( 44 | type=openapi.TYPE_OBJECT, 45 | properties={ 46 | "body": openapi.Schema( 47 | type=openapi.TYPE_STRING, 48 | description="this should not crash (request body on DELETE method)", 49 | ) 50 | }, 51 | ), 52 | ) 53 | def delete(self, *args, **kwargs): 54 | """summary from docstring 55 | 56 | description body is here, summary is not included 57 | """ 58 | pass 59 | 60 | 61 | class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): 62 | """ 63 | SnippetDetail classdoc 64 | 65 | put: 66 | put class docstring 67 | 68 | patch: 69 | patch class docstring 70 | """ 71 | 72 | queryset = Snippet.objects.all() 73 | serializer_class = SnippetSerializer 74 | pagination_class = None 75 | 76 | parser_classes = (CamelCaseJSONParser,) 77 | renderer_classes = (CamelCaseJSONRenderer,) 78 | swagger_schema = CamelCaseOperationIDAutoSchema 79 | 80 | def patch(self, request, *args, **kwargs): 81 | """patch method docstring""" 82 | return super(SnippetDetail, self).patch(request, *args, **kwargs) 83 | 84 | @swagger_auto_schema( 85 | manual_parameters=[ 86 | openapi.Parameter( 87 | name="id", 88 | in_=openapi.IN_PATH, 89 | type=openapi.TYPE_INTEGER, 90 | description="path parameter override", 91 | required=True, 92 | ), 93 | ], 94 | responses={ 95 | status.HTTP_204_NO_CONTENT: openapi.Response( 96 | description="this should not crash (response object with no schema)" 97 | ) 98 | }, 99 | ) 100 | def delete(self, request, *args, **kwargs): 101 | """delete method docstring""" 102 | return super(SnippetDetail, self).patch(request, *args, **kwargs) 103 | 104 | 105 | class SnippetViewerList(generics.ListAPIView): 106 | """SnippetViewerList classdoc""" 107 | 108 | serializer_class = SnippetViewerSerializer 109 | pagination_class = PageNumberPagination 110 | 111 | parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser) 112 | renderer_classes = (CamelCaseJSONRenderer,) 113 | swagger_schema = CamelCaseOperationIDAutoSchema 114 | lookup_url_kwarg = "snippet_pk" 115 | 116 | def get_object(self): 117 | queryset = Snippet.objects.all() 118 | 119 | # Perform the lookup filtering. 120 | lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field 121 | 122 | filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} 123 | obj = get_object_or_404(queryset, **filter_kwargs) 124 | 125 | # May raise a permission denied 126 | self.check_object_permissions(self.request, obj) 127 | 128 | return obj 129 | 130 | def get_queryset(self): 131 | return SnippetViewer.objects.filter(snippet=self.get_object()) 132 | -------------------------------------------------------------------------------- /testproj/testproj/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/testproj/__init__.py -------------------------------------------------------------------------------- /testproj/testproj/inspectors.py: -------------------------------------------------------------------------------- 1 | from drf_yasg import openapi 2 | from drf_yasg.inspectors import NotHandled, PaginatorInspector 3 | 4 | 5 | class UnknownPaginatorInspector(PaginatorInspector): 6 | def get_paginator_parameters(self, paginator): 7 | if hasattr(paginator, "paginator_query_args"): 8 | return [ 9 | openapi.Parameter( 10 | name=arg, in_=openapi.IN_QUERY, type=openapi.TYPE_STRING 11 | ) 12 | for arg in getattr(paginator, "paginator_query_args") 13 | ] 14 | 15 | return NotHandled 16 | -------------------------------------------------------------------------------- /testproj/testproj/runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class PytestTestRunner(object): 5 | """Runs pytest to discover and run tests.""" 6 | 7 | def __init__(self, verbosity=1, failfast=False, keepdb=False, **_): 8 | self.verbosity = verbosity 9 | self.failfast = failfast 10 | self.keepdb = keepdb 11 | 12 | def run_tests(self, test_labels): 13 | """Run pytest and return the exitcode. 14 | 15 | It translates some of Django's test command option to pytest's. 16 | """ 17 | import pytest 18 | 19 | argv = [] 20 | if self.verbosity == 0: 21 | argv.append("--quiet") 22 | if self.verbosity == 2: 23 | argv.append("--verbose") 24 | if self.verbosity == 3: 25 | argv.append("-vv") 26 | if self.failfast: 27 | argv.append("--exitfirst") 28 | if self.keepdb: 29 | argv.append("--reuse-db") 30 | 31 | argv.extend(test_labels) 32 | os.chdir("..") 33 | return pytest.main(argv) 34 | -------------------------------------------------------------------------------- /testproj/testproj/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/testproj/settings/__init__.py -------------------------------------------------------------------------------- /testproj/testproj/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | from django.urls import reverse_lazy 5 | 6 | from testproj.util import static_lazy 7 | 8 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | ALLOWED_HOSTS = [ 11 | "127.0.0.1", 12 | "localhost", 13 | "test.local", 14 | ] 15 | CORS_ORIGIN_ALLOW_ALL = True 16 | 17 | # Application definition 18 | 19 | INSTALLED_APPS = [ 20 | "django.contrib.admin", 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sessions", 24 | "django.contrib.messages", 25 | "django.contrib.staticfiles", 26 | "rest_framework", 27 | "rest_framework.authtoken", 28 | "oauth2_provider", 29 | "corsheaders", 30 | "drf_yasg", 31 | "snippets", 32 | "users", 33 | "articles", 34 | "todo", 35 | "people", 36 | ] 37 | 38 | MIDDLEWARE = [ 39 | "corsheaders.middleware.CorsMiddleware", 40 | "django.middleware.security.SecurityMiddleware", 41 | "django.contrib.sessions.middleware.SessionMiddleware", 42 | "django.middleware.common.CommonMiddleware", 43 | "django.middleware.csrf.CsrfViewMiddleware", 44 | "django.contrib.auth.middleware.AuthenticationMiddleware", 45 | "django.contrib.messages.middleware.MessageMiddleware", 46 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 47 | "drf_yasg.middleware.SwaggerExceptionMiddleware", 48 | ] 49 | 50 | ROOT_URLCONF = "testproj.urls" 51 | 52 | TEMPLATES = [ 53 | { 54 | "BACKEND": "django.template.backends.django.DjangoTemplates", 55 | "DIRS": [os.path.join(BASE_DIR, "testproj", "templates")], 56 | "APP_DIRS": True, 57 | "OPTIONS": { 58 | "context_processors": [ 59 | "django.template.context_processors.debug", 60 | "django.template.context_processors.request", 61 | "django.contrib.auth.context_processors.auth", 62 | "django.contrib.messages.context_processors.messages", 63 | ], 64 | }, 65 | }, 66 | ] 67 | 68 | WSGI_APPLICATION = "testproj.wsgi.application" 69 | 70 | LOGIN_URL = reverse_lazy("admin:login") 71 | 72 | # Password validation 73 | AUTH_PASSWORD_VALIDATORS = [ 74 | { 75 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 76 | }, 77 | { 78 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 79 | }, 80 | { 81 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 82 | }, 83 | { 84 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 85 | }, 86 | ] 87 | 88 | # Django Rest Framework 89 | REST_FRAMEWORK = { 90 | "DEFAULT_PERMISSION_CLASSES": [ 91 | "rest_framework.permissions.IsAuthenticated", 92 | ] 93 | } 94 | 95 | OAUTH2_CLIENT_ID = "12ee6bgxtpSEgP8TioWcHSXOiDBOUrVav4mRbVEs" 96 | OAUTH2_CLIENT_SECRET = ( 97 | "5FvYALo7W4uNnWE2ySw7Yzpkxh9PSf5GuY37RvOys00ydEyph64dbl1ECOKI9ceQ" 98 | "AKoz0JpiVQtq0DUnsxNhU3ubrJgZ9YbtiXymbLGJq8L7n4fiER7gXbXaNSbze3BN" 99 | ) 100 | OAUTH2_APP_NAME = "drf-yasg OAuth2 provider" 101 | 102 | OAUTH2_REDIRECT_URL = static_lazy("drf-yasg/swagger-ui-dist/oauth2-redirect.html") 103 | OAUTH2_AUTHORIZE_URL = reverse_lazy("oauth2_provider:authorize") 104 | OAUTH2_TOKEN_URL = reverse_lazy("oauth2_provider:token") 105 | 106 | # drf-yasg 107 | SWAGGER_SETTINGS = { 108 | "LOGIN_URL": reverse_lazy("admin:login"), 109 | "LOGOUT_URL": "/admin/logout", 110 | "PERSIST_AUTH": True, 111 | "REFETCH_SCHEMA_WITH_AUTH": True, 112 | "REFETCH_SCHEMA_ON_LOGOUT": True, 113 | "DEFAULT_INFO": "testproj.urls.swagger_info", 114 | "SECURITY_DEFINITIONS": { 115 | "Basic": {"type": "basic"}, 116 | "Bearer": { 117 | "in": "header", 118 | "name": "Authorization", 119 | "type": "apiKey", 120 | }, 121 | "OAuth2 password": { 122 | "flow": "password", 123 | "scopes": { 124 | "read": "Read everything.", 125 | "write": "Write everything,", 126 | }, 127 | "tokenUrl": OAUTH2_TOKEN_URL, 128 | "type": "oauth2", 129 | }, 130 | "Query": { 131 | "in": "query", 132 | "name": "auth", 133 | "type": "apiKey", 134 | }, 135 | }, 136 | "OAUTH2_REDIRECT_URL": OAUTH2_REDIRECT_URL, 137 | "OAUTH2_CONFIG": { 138 | "clientId": OAUTH2_CLIENT_ID, 139 | "clientSecret": OAUTH2_CLIENT_SECRET, 140 | "appName": OAUTH2_APP_NAME, 141 | }, 142 | "DEFAULT_PAGINATOR_INSPECTORS": [ 143 | "testproj.inspectors.UnknownPaginatorInspector", 144 | "drf_yasg.inspectors.DjangoRestResponsePagination", 145 | "drf_yasg.inspectors.DrfAPICompatInspector", 146 | "drf_yasg.inspectors.CoreAPICompatInspector", 147 | ], 148 | } 149 | 150 | REDOC_SETTINGS = { 151 | "SPEC_URL": ("schema-json", {"format": "json"}), 152 | } 153 | 154 | # Internationalization 155 | LANGUAGE_CODE = "en-us" 156 | TIME_ZONE = "UTC" 157 | USE_I18N = True 158 | USE_TZ = True 159 | 160 | # Static files (CSS, JavaScript, Images) 161 | STATIC_URL = "/static/" 162 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 163 | STATICFILES_DIRS = [ 164 | os.path.join(BASE_DIR, "testproj", "static"), 165 | ] 166 | 167 | # Testing 168 | TEST_RUNNER = "testproj.runner.PytestTestRunner" 169 | 170 | # Logging configuration 171 | LOGGING = { 172 | "version": 1, 173 | "disable_existing_loggers": True, 174 | "formatters": { 175 | "pipe_separated": { 176 | "format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s" 177 | } 178 | }, 179 | "handlers": { 180 | "console_log": { 181 | "level": "DEBUG", 182 | "class": "logging.StreamHandler", 183 | "stream": "ext://sys.stdout", 184 | "formatter": "pipe_separated", 185 | }, 186 | }, 187 | "loggers": { 188 | "drf_yasg": { 189 | "handlers": ["console_log"], 190 | "level": "DEBUG", 191 | "propagate": False, 192 | }, 193 | "django": { 194 | "handlers": ["console_log"], 195 | "level": "INFO", 196 | "propagate": False, 197 | }, 198 | "swagger_spec_validator": { 199 | "handlers": ["console_log"], 200 | "level": "INFO", 201 | "propagate": False, 202 | }, 203 | }, 204 | "root": { 205 | "handlers": ["console_log"], 206 | "level": "INFO", 207 | }, 208 | } 209 | -------------------------------------------------------------------------------- /testproj/testproj/settings/heroku.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import dj_database_url 4 | 5 | from .base import * # noqa: F403 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS.append(".herokuapp.com") 10 | 11 | SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") 12 | assert SECRET_KEY, "DJANGO_SECRET_KEY environment variable must be set" 13 | 14 | SESSION_COOKIE_SECURE = True 15 | CSRF_COOKIE_SECURE = True 16 | 17 | SECURE_BROWSER_XSS_FILTER = True 18 | SECURE_CONTENT_TYPE_NOSNIFF = True 19 | X_FRAME_OPTIONS = "DENY" 20 | 21 | # Simplified static file serving. 22 | # https://warehouse.python.org/project/whitenoise/ 23 | 24 | STORAGES = { 25 | "default": { 26 | "BACKEND": "django.core.files.storage.FileSystemStorage", 27 | }, 28 | "staticfiles": { 29 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 30 | }, 31 | } 32 | MIDDLEWARE.insert(0, "whitenoise.middleware.WhiteNoiseMiddleware") 33 | 34 | # Database 35 | DATABASES = {"default": dj_database_url.config(conn_max_age=600)} 36 | 37 | SILENCED_SYSTEM_CHECKS = [ 38 | "security.W004", # SECURE_HSTS_SECONDS 39 | "security.W008", # SECURE_SSL_REDIRECT 40 | ] 41 | -------------------------------------------------------------------------------- /testproj/testproj/settings/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import dj_database_url 4 | 5 | from .base import * # noqa: F403 6 | 7 | SWAGGER_SETTINGS.update({"VALIDATOR_URL": "http://localhost:8189"}) 8 | 9 | # Database 10 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 11 | 12 | db_path = os.path.join(BASE_DIR, "db.sqlite3") 13 | DATABASES = {"default": dj_database_url.parse("sqlite:///" + db_path)} 14 | 15 | # Quick-start development settings - unsuitable for production 16 | 17 | # SECURITY WARNING: keep the secret key used in production secret! 18 | # cspell:disable-next-line 19 | SECRET_KEY = "!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p" 20 | 21 | # SECURITY WARNING: don't run with debug turned on in production! 22 | DEBUG = True 23 | -------------------------------------------------------------------------------- /testproj/testproj/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/testproj/static/.gitkeep -------------------------------------------------------------------------------- /testproj/testproj/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/testproj/templates/.gitkeep -------------------------------------------------------------------------------- /testproj/testproj/urls.py: -------------------------------------------------------------------------------- 1 | import user_agents 2 | from django.contrib import admin 3 | from django.shortcuts import redirect 4 | from django.urls import include, path, re_path 5 | from rest_framework import permissions 6 | from rest_framework.decorators import api_view 7 | 8 | from drf_yasg import openapi 9 | from drf_yasg.views import get_schema_view 10 | 11 | swagger_info = openapi.Info( 12 | title="Snippets API", 13 | default_version="v1", 14 | description="""This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library. 15 | 16 | The `swagger-ui` view can be found [here](/cached/swagger). 17 | The `ReDoc` view can be found [here](/cached/redoc). 18 | The swagger YAML document can be found [here](/cached/swagger.yaml). 19 | 20 | You can log in using the pre-existing `admin` user with password `passwordadmin`.""", # noqa 21 | terms_of_service="https://www.google.com/policies/terms/", 22 | contact=openapi.Contact(email="contact@snippets.local"), 23 | license=openapi.License(name="BSD License"), 24 | ) 25 | 26 | SchemaView = get_schema_view( 27 | validators=["ssv", "flex"], 28 | public=True, 29 | permission_classes=[permissions.AllowAny], 30 | ) 31 | 32 | 33 | @api_view(["GET"]) 34 | def plain_view(request): 35 | pass 36 | 37 | 38 | def root_redirect(request): 39 | user_agent_string = request.headers.get("user-agent", "") 40 | user_agent = user_agents.parse(user_agent_string) 41 | 42 | if user_agent.is_mobile: 43 | schema_view = "cschema-redoc" 44 | else: 45 | schema_view = "cschema-swagger-ui" 46 | 47 | return redirect(schema_view, permanent=True) 48 | 49 | 50 | # urlpatterns required for settings values 51 | required_urlpatterns = [ 52 | path("admin/", admin.site.urls), 53 | path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), 54 | ] 55 | 56 | urlpatterns = [ 57 | re_path( 58 | r"^swagger\.(?Pjson|yaml)$", 59 | SchemaView.without_ui(cache_timeout=0), 60 | name="schema-json", 61 | ), 62 | path( 63 | "swagger/", 64 | SchemaView.with_ui("swagger", cache_timeout=0), 65 | name="schema-swagger-ui", 66 | ), 67 | path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), 68 | path( 69 | "redoc-old/", 70 | SchemaView.with_ui("redoc-old", cache_timeout=0), 71 | name="schema-redoc-old", 72 | ), 73 | re_path( 74 | r"^cached/swagger\.(?Pjson|yaml)$", 75 | SchemaView.without_ui(cache_timeout=None), 76 | name="cschema-json", 77 | ), 78 | path( 79 | "cached/swagger/", 80 | SchemaView.with_ui("swagger", cache_timeout=None), 81 | name="cschema-swagger-ui", 82 | ), 83 | path( 84 | "cached/redoc/", 85 | SchemaView.with_ui("redoc", cache_timeout=None), 86 | name="cschema-redoc", 87 | ), 88 | path("", root_redirect), 89 | path("snippets/", include("snippets.urls")), 90 | path("articles/", include("articles.urls")), 91 | path("users/", include("users.urls")), 92 | path("todo/", include("todo.urls")), 93 | path("people/", include("people.urls")), 94 | path("plain/", plain_view), 95 | ] + required_urlpatterns 96 | -------------------------------------------------------------------------------- /testproj/testproj/util.py: -------------------------------------------------------------------------------- 1 | from django.templatetags.static import static 2 | from django.utils.functional import lazy 3 | 4 | static_lazy = lazy(static, str) 5 | -------------------------------------------------------------------------------- /testproj/testproj/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /testproj/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/todo/__init__.py -------------------------------------------------------------------------------- /testproj/todo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-03-18 18:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Todo', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=50)), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='TodoAnother', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('title', models.CharField(max_length=50)), 27 | ('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.Todo')), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='TodoYetAnother', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('title', models.CharField(max_length=50)), 35 | ('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.TodoAnother')), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /testproj/todo/migrations/0002_todotree.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-26 13:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ('todo', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='TodoTree', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('title', models.CharField(max_length=50)), 18 | ('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, 19 | related_name='children', to='todo.TodoTree')), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /testproj/todo/migrations/0003_pack.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-04-01 00:28 2 | 3 | from decimal import Decimal 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('todo', '0002_todotree'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Pack', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('size_code', models.DecimalField(choices=[(Decimal('50'), '5x10'), (Decimal('100'), '10x10'), (Decimal('200'), '10x20')], decimal_places=3, default=Decimal('200'), max_digits=7)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testproj/todo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/todo/migrations/__init__.py -------------------------------------------------------------------------------- /testproj/todo/models.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.db import models 4 | 5 | 6 | class Todo(models.Model): 7 | title = models.CharField(max_length=50) 8 | 9 | 10 | class TodoAnother(models.Model): 11 | todo = models.ForeignKey(Todo, on_delete=models.CASCADE) 12 | title = models.CharField(max_length=50) 13 | 14 | 15 | class TodoYetAnother(models.Model): 16 | todo = models.ForeignKey(TodoAnother, on_delete=models.CASCADE) 17 | title = models.CharField(max_length=50) 18 | 19 | 20 | class TodoTree(models.Model): 21 | parent = models.ForeignKey( 22 | "self", on_delete=models.CASCADE, related_name="children", null=True 23 | ) 24 | title = models.CharField(max_length=50) 25 | 26 | 27 | class Pack(models.Model): 28 | SIZE_10x20 = Decimal(200.000) 29 | SIZE_10x10 = Decimal(100.000) 30 | SIZE_5x10 = Decimal(50.000) 31 | 32 | size_code_choices = ( 33 | (SIZE_5x10, "5x10"), 34 | (SIZE_10x10, "10x10"), 35 | (SIZE_10x20, "10x20"), 36 | ) 37 | size_code = models.DecimalField( 38 | max_digits=7, decimal_places=3, choices=size_code_choices, default=SIZE_10x20 39 | ) 40 | -------------------------------------------------------------------------------- /testproj/todo/serializer.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.utils import timezone 4 | from rest_framework import serializers 5 | from rest_framework_recursive.fields import RecursiveField 6 | 7 | from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother 8 | 9 | 10 | class TodoSerializer(serializers.ModelSerializer): 11 | class Meta: 12 | model = Todo 13 | fields = ( 14 | "title", 15 | "a_hidden_field", 16 | ) 17 | 18 | a_hidden_field = serializers.HiddenField(default=timezone.now) 19 | 20 | 21 | class TodoAnotherSerializer(serializers.ModelSerializer): 22 | todo = TodoSerializer() 23 | 24 | class Meta: 25 | model = TodoAnother 26 | fields = ("title", "todo") 27 | 28 | 29 | class TodoYetAnotherSerializer(serializers.ModelSerializer): 30 | class Meta: 31 | model = TodoYetAnother 32 | fields = ("title", "todo") 33 | depth = 2 34 | swagger_schema_fields = { 35 | "example": OrderedDict( 36 | [ 37 | ("title", "parent"), 38 | ( 39 | "todo", 40 | OrderedDict( 41 | [ 42 | ("title", "child"), 43 | ("todo", None), 44 | ] 45 | ), 46 | ), 47 | ] 48 | ) 49 | } 50 | 51 | 52 | class TodoTreeSerializer(serializers.ModelSerializer): 53 | children = serializers.ListField(child=RecursiveField(), source="children.all") 54 | many_children = RecursiveField(many=True, source="children") 55 | 56 | class Meta: 57 | model = TodoTree 58 | fields = ("id", "title", "children", "many_children") 59 | 60 | 61 | class TodoRecursiveSerializer(serializers.ModelSerializer): 62 | parent = RecursiveField(read_only=True) 63 | parent_id = serializers.PrimaryKeyRelatedField( 64 | queryset=TodoTree.objects.all(), 65 | pk_field=serializers.IntegerField(), 66 | write_only=True, 67 | allow_null=True, 68 | required=False, 69 | default=None, 70 | source="parent", 71 | ) 72 | 73 | class Meta: 74 | model = TodoTree 75 | fields = ("id", "title", "parent", "parent_id") 76 | 77 | 78 | class HarvestSerializer(serializers.ModelSerializer): 79 | class Meta: 80 | model = Pack 81 | fields = ("size_code",) 82 | read_only_fields = ("size_code",) 83 | -------------------------------------------------------------------------------- /testproj/todo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import routers 3 | 4 | from todo import views 5 | 6 | router = routers.DefaultRouter() 7 | router.register(r"", views.TodoViewSet) 8 | router.register(r"another", views.TodoAnotherViewSet) 9 | router.register(r"yetanother", views.TodoYetAnotherViewSet) 10 | router.register(r"tree", views.TodoTreeView) 11 | router.register(r"recursive", views.TodoRecursiveView, basename="todorecursivetree") 12 | router.register(r"harvest", views.HarvestViewSet) 13 | 14 | urlpatterns = router.urls 15 | 16 | urlpatterns += [ 17 | path( 18 | r"/yetanothers//", 19 | views.NestedTodoView.as_view(), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /testproj/todo/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import mixins, permissions, viewsets 2 | from rest_framework.authentication import TokenAuthentication 3 | from rest_framework.generics import RetrieveAPIView 4 | 5 | from drf_yasg.utils import swagger_auto_schema 6 | 7 | from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother 8 | from .serializer import ( 9 | HarvestSerializer, 10 | TodoAnotherSerializer, 11 | TodoRecursiveSerializer, 12 | TodoSerializer, 13 | TodoTreeSerializer, 14 | TodoYetAnotherSerializer, 15 | ) 16 | 17 | 18 | class TodoViewSet(viewsets.ReadOnlyModelViewSet): 19 | queryset = Todo.objects.all() 20 | serializer_class = TodoSerializer 21 | 22 | lookup_field = "id" 23 | lookup_value_regex = "[0-9]+" 24 | 25 | 26 | class TodoAnotherViewSet(viewsets.ReadOnlyModelViewSet): 27 | queryset = TodoAnother.objects.all() 28 | serializer_class = TodoAnotherSerializer 29 | 30 | 31 | class TodoYetAnotherViewSet(viewsets.ReadOnlyModelViewSet): 32 | queryset = TodoYetAnother.objects.all() 33 | serializer_class = TodoYetAnotherSerializer 34 | 35 | 36 | class NestedTodoView(RetrieveAPIView): 37 | serializer_class = TodoYetAnotherSerializer 38 | 39 | 40 | class TodoTreeView(viewsets.ReadOnlyModelViewSet): 41 | queryset = TodoTree.objects.all() 42 | 43 | def get_serializer_class(self): 44 | if getattr(self, "swagger_fake_view", False): 45 | return TodoTreeSerializer 46 | 47 | raise NotImplementedError("must not call this") 48 | 49 | 50 | class TodoRecursiveView(viewsets.ModelViewSet): 51 | queryset = TodoTree.objects.all() 52 | 53 | def get_serializer(self, *args, **kwargs): 54 | raise NotImplementedError("must not call this") 55 | 56 | def get_serializer_class(self): 57 | raise NotImplementedError("must not call this") 58 | 59 | def get_serializer_context(self): 60 | raise NotImplementedError("must not call this") 61 | 62 | @swagger_auto_schema(request_body=TodoRecursiveSerializer) 63 | def create(self, request, *args, **kwargs): 64 | return super(TodoRecursiveView, self).create(request, *args, **kwargs) 65 | 66 | @swagger_auto_schema(responses={200: None, 302: "Redirect somewhere"}) 67 | def retrieve(self, request, *args, **kwargs): 68 | return super(TodoRecursiveView, self).retrieve(request, *args, **kwargs) 69 | 70 | @swagger_auto_schema(request_body=TodoRecursiveSerializer) 71 | def update(self, request, *args, **kwargs): 72 | return super(TodoRecursiveView, self).update(request, *args, **kwargs) 73 | 74 | @swagger_auto_schema(request_body=TodoRecursiveSerializer) 75 | def partial_update(self, request, *args, **kwargs): 76 | return super(TodoRecursiveView, self).update(request, *args, **kwargs) 77 | 78 | def destroy(self, request, *args, **kwargs): 79 | return super(TodoRecursiveView, self).destroy(request, *args, **kwargs) 80 | 81 | @swagger_auto_schema(responses={200: TodoRecursiveSerializer(many=True)}) 82 | def list(self, request, *args, **kwargs): 83 | return super(TodoRecursiveView, self).list(request, *args, **kwargs) 84 | 85 | 86 | class HarvestViewSet( 87 | mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet 88 | ): 89 | queryset = Pack.objects.all() 90 | serializer_class = HarvestSerializer 91 | permission_classes = (permissions.IsAuthenticated,) 92 | authentication_classes = (TokenAuthentication,) 93 | 94 | def perform_update(self, serializer): 95 | pass 96 | -------------------------------------------------------------------------------- /testproj/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/users/__init__.py -------------------------------------------------------------------------------- /testproj/users/method_serializers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import uuid 4 | 5 | from rest_framework import serializers 6 | 7 | 8 | class Unknown(object): 9 | pass 10 | 11 | 12 | class MethodFieldExampleSerializer(serializers.Serializer): 13 | """ 14 | Implementation of SerializerMethodField using type hinting for Python >= 3.5 15 | """ 16 | 17 | hinted_bool = serializers.SerializerMethodField( 18 | help_text="the type hint on the method should determine this to be a bool" 19 | ) 20 | 21 | def get_hinted_bool(self, obj) -> bool: 22 | return True 23 | 24 | hinted_int = serializers.SerializerMethodField( 25 | help_text="the type hint on the method should determine this to be an integer" 26 | ) 27 | 28 | def get_hinted_int(self, obj) -> int: 29 | return 1 30 | 31 | hinted_float = serializers.SerializerMethodField( 32 | help_text="the type hint on the method should determine this to be a number" 33 | ) 34 | 35 | def get_hinted_float(self, obj) -> float: 36 | return 1.0 37 | 38 | hinted_decimal = serializers.SerializerMethodField( 39 | help_text="the type hint on the method should determine this to be a decimal" 40 | ) 41 | 42 | def get_hinted_decimal(self, obj) -> decimal.Decimal: 43 | return decimal.Decimal(1) 44 | 45 | hinted_datetime = serializers.SerializerMethodField( 46 | help_text="the type hint on the method should determine this to be a datetime" 47 | ) 48 | 49 | def get_hinted_datetime(self, obj) -> datetime.datetime: 50 | return datetime.datetime.now() 51 | 52 | hinted_date = serializers.SerializerMethodField( 53 | help_text="the type hint on the method should determine this to be a date" 54 | ) 55 | 56 | def get_hinted_date(self, obj) -> datetime.date: 57 | return datetime.date.today() 58 | 59 | hinted_uuid = serializers.SerializerMethodField( 60 | help_text="the type hint on the method should determine this to be a uuid" 61 | ) 62 | 63 | def get_hinted_uuid(self, obj) -> uuid.UUID: 64 | return uuid.uuid4() 65 | 66 | hinted_unknown = serializers.SerializerMethodField( 67 | help_text="type hint is unknown, so is expected to fallback to string" 68 | ) 69 | 70 | def get_hinted_unknown(self, obj) -> Unknown: 71 | return Unknown() 72 | 73 | non_hinted_number = serializers.SerializerMethodField( 74 | help_text="No hint on the method, so this is expected to fallback to string" 75 | ) 76 | 77 | def get_non_hinted_number(self, obj): 78 | return 1.0 79 | -------------------------------------------------------------------------------- /testproj/users/migrations/0001_create_admin_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-19 08:07 2 | import sys 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.hashers import make_password 6 | from django.db import migrations, IntegrityError, transaction 7 | 8 | 9 | def add_default_user(apps, schema_editor): 10 | username = 'admin' 11 | email = 'admin@admin.admin' 12 | password = 'passwordadmin' 13 | User = apps.get_model(settings.AUTH_USER_MODEL) 14 | 15 | try: 16 | with transaction.atomic(): 17 | admin = User( 18 | username=username, 19 | email=email, 20 | password=make_password(password), 21 | is_superuser=True, 22 | is_staff=True 23 | ) 24 | admin.save() 25 | except IntegrityError: 26 | sys.stdout.write(" User '%s <%s>' already exists..." % (username, email)) 27 | else: 28 | sys.stdout.write(" Created superuser '%s <%s>' with password '%s'!" % (username, email, password)) 29 | 30 | 31 | class Migration(migrations.Migration): 32 | dependencies = [ 33 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 34 | ] 35 | 36 | operations = [ 37 | migrations.RunPython(add_default_user) 38 | ] 39 | -------------------------------------------------------------------------------- /testproj/users/migrations/0002_setup_oauth2_apps.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-19 07:57 2 | from django.conf import settings 3 | from django.db import migrations 4 | 5 | 6 | def add_oauth_apps(apps, schema_editor): 7 | # We can't import the Person model directly as it may be a newer 8 | # version than this migration expects. We use the historical version. 9 | User = apps.get_model(settings.AUTH_USER_MODEL) 10 | Application = apps.get_model('oauth2_provider', 'application') 11 | 12 | user = User.objects.get(username='admin') 13 | 14 | oauth2_apps = [ 15 | { 16 | "user": user, 17 | "client_type": "public", 18 | "authorization_grant_type": "password", 19 | "client_id": settings.OAUTH2_CLIENT_ID, 20 | "client_secret": settings.OAUTH2_CLIENT_SECRET, 21 | "redirect_uris": settings.OAUTH2_REDIRECT_URL, 22 | "name": settings.OAUTH2_APP_NAME 23 | } 24 | ] 25 | 26 | for app in oauth2_apps: 27 | Application.objects.get_or_create(client_id=app['client_id'], defaults=app) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | dependencies = [ 32 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 33 | ('oauth2_provider', '0002_auto_20190406_1805'), 34 | ('users', '0001_create_admin_user'), 35 | ] 36 | 37 | operations = [ 38 | migrations.RunPython(add_oauth_apps) 39 | ] 40 | -------------------------------------------------------------------------------- /testproj/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/users/migrations/__init__.py -------------------------------------------------------------------------------- /testproj/users/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/testproj/users/models.py -------------------------------------------------------------------------------- /testproj/users/serializers.py: -------------------------------------------------------------------------------- 1 | import typing # noqa: F401 2 | 3 | from django.contrib.auth.models import User 4 | from rest_framework import serializers 5 | 6 | from drf_yasg.utils import swagger_serializer_method 7 | from snippets.models import Snippet 8 | 9 | from .method_serializers import MethodFieldExampleSerializer 10 | 11 | 12 | class OtherStuffSerializer(serializers.Serializer): 13 | foo = serializers.CharField() 14 | 15 | 16 | class UserSerializer(serializers.ModelSerializer): 17 | snippets = serializers.PrimaryKeyRelatedField( 18 | many=True, queryset=Snippet.objects.all() 19 | ) 20 | article_slugs = serializers.SlugRelatedField( 21 | read_only=True, slug_field="slug", many=True, source="articles" 22 | ) 23 | last_connected_ip = serializers.IPAddressField( 24 | help_text="i'm out of ideas", protocol="ipv4", read_only=True 25 | ) 26 | last_connected_at = serializers.DateField(help_text="really?", read_only=True) 27 | 28 | other_stuff = serializers.SerializerMethodField( 29 | help_text="the decorator should determine the serializer class for this" 30 | ) 31 | 32 | hint_example = MethodFieldExampleSerializer() 33 | 34 | @swagger_serializer_method(serializer_or_field=OtherStuffSerializer) 35 | def get_other_stuff(self, obj): 36 | """ 37 | method_field that uses a serializer internally. 38 | 39 | By using the decorator, we can tell drf-yasg how to represent this in Swagger 40 | :param obj: 41 | :return: 42 | """ 43 | return OtherStuffSerializer().data 44 | 45 | help_text_example_1 = serializers.SerializerMethodField( 46 | help_text="help text on field is set, so this should appear in swagger" 47 | ) 48 | 49 | @swagger_serializer_method( 50 | serializer_or_field=serializers.IntegerField( 51 | help_text=( 52 | "decorated instance help_text shouldn't appear in swagger because " 53 | "field has priority" 54 | ) 55 | ) 56 | ) 57 | def get_help_text_example_1(self): 58 | """ 59 | method docstring shouldn't appear in swagger because field has priority 60 | :return: 61 | """ 62 | return 1 63 | 64 | help_text_example_2 = serializers.SerializerMethodField() 65 | 66 | @swagger_serializer_method( 67 | serializer_or_field=serializers.IntegerField( 68 | help_text="instance help_text is set, so should appear in swagger" 69 | ) 70 | ) 71 | def get_help_text_example_2(self): 72 | """ 73 | method docstring shouldn't appear in swagger because decorator has priority 74 | :return: 75 | """ 76 | return 1 77 | 78 | help_text_example_3 = serializers.SerializerMethodField() 79 | 80 | @swagger_serializer_method(serializer_or_field=serializers.IntegerField()) 81 | def get_help_text_example_3(self): 82 | """ 83 | docstring is set so should appear in swagger as fallback 84 | :return: 85 | """ 86 | return 1 87 | 88 | class Meta: 89 | model = User 90 | fields = ( 91 | "id", 92 | "username", 93 | "email", 94 | "articles", 95 | "snippets", 96 | "last_connected_ip", 97 | "last_connected_at", 98 | "article_slugs", 99 | "other_stuff", 100 | "hint_example", 101 | "help_text_example_1", 102 | "help_text_example_2", 103 | "help_text_example_3", 104 | ) 105 | 106 | ref_name = "UserSerializer" 107 | 108 | 109 | class UserListQuerySerializer(serializers.Serializer): 110 | username = serializers.CharField( 111 | help_text="this field is generated from a query_serializer", required=False 112 | ) 113 | is_staff = serializers.BooleanField(help_text="this one too!", required=False) 114 | styles = serializers.MultipleChoiceField( 115 | help_text="and this one is fancy!", choices=("a", "b", "c", "d") 116 | ) 117 | -------------------------------------------------------------------------------- /testproj/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from users import views 4 | 5 | urlpatterns = [ 6 | path("", views.UserList.as_view()), 7 | path("/", views.user_detail), 8 | path("/test_dummy", views.test_view_with_dummy_schema), 9 | ] 10 | -------------------------------------------------------------------------------- /testproj/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import status 3 | from rest_framework.decorators import api_view 4 | from rest_framework.generics import get_object_or_404 5 | from rest_framework.response import Response 6 | from rest_framework.views import APIView 7 | 8 | from drf_yasg import openapi 9 | from drf_yasg.utils import swagger_auto_schema 10 | from users.serializers import UserListQuerySerializer, UserSerializer 11 | 12 | 13 | class UserList(APIView): 14 | """UserList cbv classdoc""" 15 | 16 | @swagger_auto_schema( 17 | query_serializer=UserListQuerySerializer, 18 | responses={200: UserSerializer(many=True)}, 19 | tags=["Users"], 20 | ) 21 | def get(self, request): 22 | queryset = User.objects.all() 23 | serializer = UserSerializer(queryset, many=True) 24 | return Response(serializer.data) 25 | 26 | @swagger_auto_schema( 27 | operation_description="apiview post description override", 28 | request_body=openapi.Schema( 29 | type=openapi.TYPE_OBJECT, 30 | required=["username"], 31 | properties={"username": openapi.Schema(type=openapi.TYPE_STRING)}, 32 | ), 33 | security=[], 34 | tags=["Users"], 35 | ) 36 | def post(self, request): 37 | serializer = UserSerializer(request.data) 38 | serializer.is_valid(raise_exception=True) 39 | serializer.save() 40 | return Response(serializer.data, status=status.HTTP_201_CREATED) 41 | 42 | @swagger_auto_schema( 43 | operation_id="users_dummy", 44 | operation_description="dummy operation", 45 | tags=["Users"], 46 | ) 47 | def patch(self, request): 48 | pass 49 | 50 | 51 | @swagger_auto_schema(method="put", request_body=UserSerializer, tags=["Users"]) 52 | @swagger_auto_schema( 53 | methods=["get"], 54 | manual_parameters=[ 55 | openapi.Parameter( 56 | "test", openapi.IN_QUERY, "test manual param", type=openapi.TYPE_BOOLEAN 57 | ), 58 | openapi.Parameter( 59 | "test_array", 60 | openapi.IN_QUERY, 61 | "test query array arg", 62 | type=openapi.TYPE_ARRAY, 63 | items=openapi.Items(type=openapi.TYPE_STRING), 64 | required=True, 65 | collection_format="multi", 66 | ), 67 | ], 68 | responses={ 69 | 200: openapi.Response("response description", UserSerializer), 70 | }, 71 | tags=["Users"], 72 | ) 73 | @api_view(["GET", "PUT"]) 74 | def user_detail(request, pk): 75 | """user_detail fbv docstring""" 76 | user = get_object_or_404(User.objects, pk=pk) 77 | serializer = UserSerializer(user) 78 | return Response(serializer.data) 79 | 80 | 81 | class DummyAutoSchema: 82 | def __init__(self, *args, **kwargs): 83 | pass 84 | 85 | def get_operation(self, keys): 86 | pass 87 | 88 | 89 | @swagger_auto_schema(methods=["get"], auto_schema=DummyAutoSchema) 90 | @swagger_auto_schema(methods=["PUT"], auto_schema=None) 91 | @api_view(["GET", "PUT"]) 92 | def test_view_with_dummy_schema(request, pk): 93 | return Response({}) 94 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | from collections import OrderedDict 5 | from io import StringIO 6 | 7 | import pytest 8 | from datadiff.tools import assert_equal 9 | from django.contrib.auth.models import User 10 | from django.core.management import call_command 11 | from rest_framework.test import APIRequestFactory 12 | from rest_framework.views import APIView 13 | 14 | from drf_yasg import codecs, openapi 15 | from drf_yasg.codecs import yaml_sane_dump, yaml_sane_load 16 | from drf_yasg.generators import OpenAPISchemaGenerator 17 | 18 | 19 | @pytest.fixture 20 | def mock_schema_request(db): 21 | from rest_framework.test import force_authenticate 22 | 23 | factory = APIRequestFactory() 24 | user = User.objects.get(username="admin") 25 | request = factory.get("/swagger.json") 26 | force_authenticate(request, user=user) 27 | request = APIView().initialize_request(request) 28 | return request 29 | 30 | 31 | @pytest.fixture 32 | def codec_json(): 33 | return codecs.OpenAPICodecJson(["flex", "ssv"]) 34 | 35 | 36 | @pytest.fixture 37 | def codec_yaml(): 38 | return codecs.OpenAPICodecYaml(["ssv", "flex"]) 39 | 40 | 41 | @pytest.fixture 42 | def swagger(mock_schema_request): 43 | generator = OpenAPISchemaGenerator( 44 | info=openapi.Info(title="Test generator", default_version="v1"), 45 | version="v2", 46 | ) 47 | return generator.get_schema(mock_schema_request, True) 48 | 49 | 50 | @pytest.fixture 51 | def swagger_dict(swagger, codec_json): 52 | json_bytes = codec_json.encode(swagger) 53 | return json.loads(json_bytes.decode("utf-8"), object_pairs_hook=OrderedDict) 54 | 55 | 56 | @pytest.fixture 57 | def validate_schema(): 58 | def validate_schema(swagger): 59 | try: 60 | from flex.core import parse as validate_flex 61 | 62 | validate_flex(copy.deepcopy(swagger)) 63 | except ImportError: 64 | pass 65 | 66 | from swagger_spec_validator.validator20 import validate_spec as validate_ssv 67 | 68 | validate_ssv(copy.deepcopy(swagger)) 69 | 70 | return validate_schema 71 | 72 | 73 | @pytest.fixture 74 | def call_generate_swagger(): 75 | def call_generate_swagger( 76 | output_file="-", 77 | overwrite=False, 78 | format="", 79 | api_url="", 80 | mock=False, 81 | user=None, 82 | private=False, 83 | generator_class_name="", 84 | **kwargs, 85 | ): 86 | out = StringIO() 87 | call_command( 88 | "generate_swagger", 89 | stdout=out, 90 | output_file=output_file, 91 | overwrite=overwrite, 92 | format=format, 93 | api_url=api_url, 94 | mock=mock, 95 | user=user, 96 | private=private, 97 | generator_class_name=generator_class_name, 98 | **kwargs, 99 | ) 100 | return out.getvalue() 101 | 102 | return call_generate_swagger 103 | 104 | 105 | @pytest.fixture 106 | def compare_schemas(): 107 | def compare_schemas(schema1, schema2): 108 | schema1 = OrderedDict(schema1) 109 | schema2 = OrderedDict(schema2) 110 | ignore = ["info", "host", "schemes", "basePath", "securityDefinitions"] 111 | for attr in ignore: 112 | schema1.pop(attr, None) 113 | schema2.pop(attr, None) 114 | 115 | # print diff between YAML strings because it's prettier 116 | assert_equal( 117 | yaml_sane_dump(schema1, binary=False), yaml_sane_dump(schema2, binary=False) 118 | ) 119 | 120 | return compare_schemas 121 | 122 | 123 | @pytest.fixture 124 | def swagger_settings(settings): 125 | swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS) 126 | settings.SWAGGER_SETTINGS = swagger_settings 127 | return swagger_settings 128 | 129 | 130 | @pytest.fixture 131 | def redoc_settings(settings): 132 | redoc_settings = copy.deepcopy(settings.REDOC_SETTINGS) 133 | settings.REDOC_SETTINGS = redoc_settings 134 | return redoc_settings 135 | 136 | 137 | @pytest.fixture 138 | def reference_schema(): 139 | with open(os.path.join(os.path.dirname(__file__), "reference.yaml")) as reference: 140 | return yaml_sane_load(reference) 141 | -------------------------------------------------------------------------------- /tests/test_form_parameters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import path 3 | from django.utils.decorators import method_decorator 4 | from rest_framework.authtoken.views import ObtainAuthToken 5 | from rest_framework.settings import api_settings 6 | 7 | from drf_yasg import openapi 8 | from drf_yasg.errors import SwaggerGenerationError 9 | from drf_yasg.generators import OpenAPISchemaGenerator 10 | from drf_yasg.utils import swagger_auto_schema 11 | 12 | 13 | def test_no_form_parameters_with_non_form_parsers(): 14 | # see https://github.com/axnsan12/drf-yasg/issues/270 15 | # test that manual form parameters for views that haven't set 16 | # all their parsers classes to form parsers are not allowed 17 | # even when the request body is empty 18 | 19 | @method_decorator( 20 | name="post", 21 | decorator=swagger_auto_schema( 22 | operation_description="Logins a user and returns a token", 23 | manual_parameters=[ 24 | openapi.Parameter( 25 | "username", 26 | openapi.IN_FORM, 27 | required=True, 28 | type=openapi.TYPE_STRING, 29 | description="Valid username or email for authentication", 30 | ), 31 | ], 32 | ), 33 | ) 34 | class CustomObtainAuthToken(ObtainAuthToken): 35 | throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES 36 | 37 | urlpatterns = [ 38 | path("token/", CustomObtainAuthToken.as_view()), 39 | ] 40 | 41 | generator = OpenAPISchemaGenerator( 42 | info=openapi.Info(title="Test generator", default_version="v1"), 43 | patterns=urlpatterns, 44 | ) 45 | 46 | with pytest.raises(SwaggerGenerationError): 47 | generator.get_schema(None, True) 48 | -------------------------------------------------------------------------------- /tests/test_get_basic_type_info_from_hint.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import uuid 3 | from typing import Dict, List, Optional, Set, Union 4 | 5 | import pytest 6 | 7 | from drf_yasg import openapi 8 | from drf_yasg.inspectors.field import get_basic_type_info_from_hint 9 | 10 | python39_generics_tests = [] 11 | if sys.version_info >= (3, 9): 12 | python39_generics_tests = [ 13 | (dict[int, int], {"type": openapi.TYPE_OBJECT, "format": None}), 14 | ( 15 | list[bool], 16 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_BOOLEAN)}, 17 | ), 18 | ] 19 | 20 | 21 | python310_union_tests = [] 22 | if sys.version_info >= (3, 10): 23 | # # New PEP 604 union syntax in Python 3.10+ 24 | python310_union_tests = [ 25 | ( 26 | bool | None, 27 | {"type": openapi.TYPE_BOOLEAN, "format": None, "x-nullable": True}, 28 | ), 29 | ( 30 | list[int] | None, 31 | { 32 | "type": openapi.TYPE_ARRAY, 33 | "items": openapi.Items(openapi.TYPE_INTEGER), 34 | "x-nullable": True, 35 | }, 36 | ), 37 | # Following cases are not 100% correct, but it should work somehow and not crash 38 | (int | float, None), 39 | ] 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "hint_class, expected_swagger_type_info", 44 | [ 45 | (int, {"type": openapi.TYPE_INTEGER, "format": None}), 46 | (str, {"type": openapi.TYPE_STRING, "format": None}), 47 | (bool, {"type": openapi.TYPE_BOOLEAN, "format": None}), 48 | (dict, {"type": openapi.TYPE_OBJECT, "format": None}), 49 | (Dict[int, int], {"type": openapi.TYPE_OBJECT, "format": None}), 50 | (uuid.UUID, {"type": openapi.TYPE_STRING, "format": openapi.FORMAT_UUID}), 51 | ( 52 | list, 53 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_STRING)}, 54 | ), 55 | ( 56 | List[int], 57 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_INTEGER)}, 58 | ), 59 | ( 60 | List[str], 61 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_STRING)}, 62 | ), 63 | ( 64 | List[bool], 65 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_BOOLEAN)}, 66 | ), 67 | ( 68 | Set[int], 69 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_INTEGER)}, 70 | ), 71 | ( 72 | Optional[bool], 73 | {"type": openapi.TYPE_BOOLEAN, "format": None, "x-nullable": True}, 74 | ), 75 | ( 76 | Optional[List[int]], 77 | { 78 | "type": openapi.TYPE_ARRAY, 79 | "items": openapi.Items(openapi.TYPE_INTEGER), 80 | "x-nullable": True, 81 | }, 82 | ), 83 | ( 84 | Union[List[int], type(None)], 85 | { 86 | "type": openapi.TYPE_ARRAY, 87 | "items": openapi.Items(openapi.TYPE_INTEGER), 88 | "x-nullable": True, 89 | }, 90 | ), 91 | # Following cases are not 100% correct, but it should work somehow and not crash 92 | (Union[int, float], None), 93 | ( 94 | List, 95 | {"type": openapi.TYPE_ARRAY, "items": openapi.Items(openapi.TYPE_STRING)}, 96 | ), 97 | ("SomeType", None), 98 | (type("SomeType", (object,), {}), None), 99 | (None, None), 100 | (6, None), 101 | ] 102 | + python39_generics_tests 103 | + python310_union_tests, 104 | ) 105 | def test_get_basic_type_info_from_hint(hint_class, expected_swagger_type_info): 106 | type_info = get_basic_type_info_from_hint(hint_class) 107 | assert type_info == expected_swagger_type_info 108 | -------------------------------------------------------------------------------- /tests/test_management.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import string 5 | import tempfile 6 | from collections import OrderedDict 7 | 8 | import pytest 9 | 10 | from drf_yasg import openapi 11 | from drf_yasg.codecs import yaml_sane_load 12 | from drf_yasg.generators import OpenAPISchemaGenerator 13 | 14 | 15 | def test_reference_schema(call_generate_swagger, db, reference_schema): 16 | output = call_generate_swagger( 17 | format="yaml", api_url="http://test.local:8002/", user="admin" 18 | ) 19 | output_schema = yaml_sane_load(output) 20 | assert output_schema == reference_schema 21 | 22 | 23 | def test_non_public(call_generate_swagger, db): 24 | output = call_generate_swagger( 25 | format="yaml", api_url="http://test.local:8002/", private=True 26 | ) 27 | output_schema = yaml_sane_load(output) 28 | assert len(output_schema["paths"]) == 0 29 | 30 | 31 | def test_no_mock(call_generate_swagger, db): 32 | output = call_generate_swagger() 33 | output_schema = json.loads(output, object_pairs_hook=OrderedDict) 34 | assert len(output_schema["paths"]) > 0 35 | 36 | 37 | class EmptySchemaGenerator(OpenAPISchemaGenerator): 38 | def get_paths(self, endpoints, components, request, public): 39 | return openapi.Paths(paths={}), "" 40 | 41 | 42 | def test_generator_class(call_generate_swagger, db): 43 | output = call_generate_swagger( 44 | generator_class_name="test_management.EmptySchemaGenerator" 45 | ) 46 | output_schema = json.loads(output, object_pairs_hook=OrderedDict) 47 | assert len(output_schema["paths"]) == 0 48 | 49 | 50 | def silent_remove(filename): 51 | try: 52 | os.remove(filename) 53 | except OSError: 54 | pass 55 | 56 | 57 | def test_file_output(call_generate_swagger, db): 58 | prefix = os.path.join(tempfile.gettempdir(), tempfile.gettempprefix()) 59 | name = "".join( 60 | random.choice(string.ascii_lowercase + string.digits) for _ in range(8) 61 | ) 62 | yaml_file = prefix + name + ".yaml" 63 | json_file = prefix + name + ".json" 64 | other_file = prefix + name + ".txt" 65 | 66 | try: 67 | # when called with output file nothing should be written to stdout 68 | assert call_generate_swagger(output_file=yaml_file) == "" 69 | assert call_generate_swagger(output_file=json_file) == "" 70 | assert call_generate_swagger(output_file=other_file) == "" 71 | 72 | with pytest.raises(OSError): 73 | # a second call should fail because file exists 74 | call_generate_swagger(output_file=yaml_file) 75 | 76 | # a second call with overwrite should still succeed 77 | assert call_generate_swagger(output_file=json_file, overwrite=True) == "" 78 | 79 | with open(yaml_file) as f: 80 | content = f.read() 81 | # YAML is a superset of JSON - that means we have to check that 82 | # the file is really YAML and not just JSON parsed by the YAML parser 83 | with pytest.raises(ValueError): 84 | json.loads(content) 85 | output_yaml = yaml_sane_load(content) 86 | with open(json_file) as f: 87 | output_json = json.load(f, object_pairs_hook=OrderedDict) 88 | with open(other_file) as f: 89 | output_other = json.load(f, object_pairs_hook=OrderedDict) 90 | 91 | assert output_yaml == output_json == output_other 92 | finally: 93 | silent_remove(yaml_file) 94 | silent_remove(json_file) 95 | silent_remove(other_file) 96 | -------------------------------------------------------------------------------- /tests/test_reference_schema.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import json 3 | from collections import OrderedDict 4 | 5 | from drf_yasg import openapi 6 | from drf_yasg.generators import OpenAPISchemaGenerator 7 | from drf_yasg.inspectors import ( 8 | FieldInspector, 9 | FilterInspector, 10 | PaginatorInspector, 11 | SerializerInspector, 12 | ) 13 | 14 | 15 | def test_reference_schema(swagger_dict, reference_schema, compare_schemas): 16 | compare_schemas(swagger_dict, reference_schema) 17 | 18 | 19 | class VerisonEnum(enum.Enum): 20 | V1 = "v1" 21 | V2 = "v2" 22 | 23 | 24 | class NoOpFieldInspector(FieldInspector): 25 | pass 26 | 27 | 28 | class NoOpSerializerInspector(SerializerInspector): 29 | pass 30 | 31 | 32 | class NoOpFilterInspector(FilterInspector): 33 | pass 34 | 35 | 36 | class NoOpPaginatorInspector(PaginatorInspector): 37 | pass 38 | 39 | 40 | def test_noop_inspectors( 41 | swagger_settings, mock_schema_request, codec_json, reference_schema, compare_schemas 42 | ): 43 | from drf_yasg import app_settings 44 | 45 | def set_inspectors(inspectors, setting_name): 46 | inspectors = [__name__ + "." + inspector.__name__ for inspector in inspectors] 47 | swagger_settings[setting_name] = ( 48 | inspectors + app_settings.SWAGGER_DEFAULTS[setting_name] 49 | ) 50 | 51 | set_inspectors( 52 | [NoOpFieldInspector, NoOpSerializerInspector], "DEFAULT_FIELD_INSPECTORS" 53 | ) 54 | set_inspectors([NoOpFilterInspector], "DEFAULT_FILTER_INSPECTORS") 55 | set_inspectors([NoOpPaginatorInspector], "DEFAULT_PAGINATOR_INSPECTORS") 56 | 57 | generator = OpenAPISchemaGenerator( 58 | info=openapi.Info(title="Test generator", default_version=VerisonEnum.V1), 59 | version=VerisonEnum.V2, 60 | ) 61 | swagger = generator.get_schema(mock_schema_request, True) 62 | 63 | json_bytes = codec_json.encode(swagger) 64 | swagger_dict = json.loads(json_bytes.decode("utf-8"), object_pairs_hook=OrderedDict) 65 | compare_schemas(swagger_dict, reference_schema) 66 | 67 | 68 | def test_no_nested_model(swagger_dict): 69 | # ForeignKey models in deep ModelViewSets might wrongly be labeled as 'Nested' in 70 | # the definitions section see https://github.com/axnsan12/drf-yasg/issues/59 71 | assert "Nested" not in swagger_dict["definitions"] 72 | -------------------------------------------------------------------------------- /tests/test_referenceresolver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from drf_yasg.openapi import ReferenceResolver 4 | 5 | 6 | def test_basic(): 7 | scopes = ["s1", "s2"] 8 | rr = ReferenceResolver(*scopes, force_init=True) 9 | assert scopes == rr.scopes == list(rr.keys()) == list(rr) 10 | rr.set("o1", 1, scope="s1") 11 | assert rr.has("o1", scope="s1") 12 | assert rr.get("o1", scope="s1") == 1 13 | 14 | rr.setdefault("o1", lambda: 2, scope="s1") 15 | assert rr.get("o1", scope="s1") == 1 16 | assert not rr.has("o1", scope="s2") 17 | 18 | rr.setdefault("o3", lambda: 3, scope="s2") 19 | assert rr.get("o3", scope="s2") == 3 20 | 21 | assert rr["s1"] == {"o1": 1} 22 | assert dict(rr) == {"s1": {"o1": 1}, "s2": {"o3": 3}} 23 | assert str(rr) == str(dict(rr)) 24 | 25 | 26 | def test_scoped(): 27 | scopes = ["s1", "s2"] 28 | rr = ReferenceResolver(*scopes, force_init=True) 29 | r1 = rr.with_scope("s1") 30 | r2 = rr.with_scope("s2") 31 | with pytest.raises(AssertionError): 32 | rr.with_scope("bad") 33 | 34 | assert r1.scopes == ["s1"] 35 | assert list(r1.keys()) == list(r1) == [] 36 | 37 | r2.set("o2", 2) 38 | assert r2.scopes == ["s2"] 39 | assert list(r2.keys()) == list(r2) == ["o2"] 40 | assert r2["o2"] == 2 41 | 42 | with pytest.raises(AssertionError): 43 | r2.get("o2", scope="s1") 44 | 45 | assert rr.get("o2", scope="s2") == 2 46 | -------------------------------------------------------------------------------- /tests/test_renderer_settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from drf_yasg import renderers 6 | 7 | 8 | def _check_swagger_setting(swagger, setting, expected): 9 | context = {} 10 | renderer = renderers.SwaggerUIRenderer() 11 | renderer.set_context(context, swagger) 12 | 13 | swagger_settings = json.loads(context["swagger_settings"]) 14 | assert swagger_settings[setting] == expected 15 | 16 | 17 | def _check_setting(swagger, setting, expected): 18 | context = {} 19 | renderer = renderers.SwaggerUIRenderer() 20 | renderer.set_context(context, swagger) 21 | assert context[setting] == expected 22 | 23 | 24 | def test_validator_url(swagger_settings, swagger): 25 | swagger_settings["VALIDATOR_URL"] = None 26 | _check_swagger_setting(swagger, "validatorUrl", None) 27 | 28 | swagger_settings["VALIDATOR_URL"] = "http://not.none/" 29 | _check_swagger_setting(swagger, "validatorUrl", "http://not.none/") 30 | 31 | with pytest.raises(KeyError): 32 | swagger_settings["VALIDATOR_URL"] = "" 33 | _check_swagger_setting(swagger, "validatorUrl", None) 34 | 35 | 36 | @pytest.mark.urls("urlconfs.login_test_urls") 37 | def test_login_logout(swagger_settings, swagger): 38 | swagger_settings["LOGIN_URL"] = "login" 39 | _check_setting(swagger, "LOGIN_URL", "/test/login") 40 | 41 | swagger_settings["LOGOUT_URL"] = "logout" 42 | _check_setting(swagger, "LOGOUT_URL", "/test/logout") 43 | 44 | with pytest.raises(KeyError): 45 | swagger_settings["LOGIN_URL"] = None 46 | _check_setting(swagger, "LOGIN_URL", None) 47 | 48 | with pytest.raises(KeyError): 49 | swagger_settings["LOGOUT_URL"] = None 50 | _check_setting(swagger, "LOGOUT_URL", None) 51 | -------------------------------------------------------------------------------- /tests/test_swaggerdict.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from random import shuffle 3 | 4 | from drf_yasg import openapi 5 | 6 | 7 | def test_vendor_extensions(): 8 | """Any attribute starting with x_ should map to a vendor property of the form 9 | x-camelCase""" 10 | sd = openapi.SwaggerDict(x_vendor_ext_1="test") 11 | sd.x_vendor_ext_2 = "test" 12 | assert "x-vendorExt1" in sd 13 | assert sd.x_vendor_ext_1 == "test" 14 | assert sd["x-vendorExt2"] == "test" 15 | 16 | del sd.x_vendor_ext_1 17 | assert "x-vendorExt1" not in sd 18 | 19 | 20 | def test_ref(): 21 | """The attribute 'ref' maps to the swagger key '$ref'""" 22 | sd = openapi.SwaggerDict(ref="reftest") 23 | assert "$ref" in sd 24 | assert sd["$ref"] == sd.ref == "reftest" 25 | 26 | del sd["$ref"] 27 | assert not hasattr(sd, "ref") 28 | 29 | 30 | def test_leading_underscore_ignored(): 31 | """Attributes with a leading underscore are set on the object as-is and are not 32 | added to its dict form""" 33 | sd = openapi.SwaggerDict(_private_attr_1="not_camelized") 34 | initial_len = len(sd) 35 | sd._nope = "not camelized either" 36 | assert len(sd) == initial_len 37 | assert "privateAttr1" not in sd 38 | assert sd._private_attr_1 == "not_camelized" 39 | assert "_private_attr_1" not in sd 40 | assert hasattr(sd, "_nope") 41 | 42 | del sd._nope 43 | assert not hasattr(sd, "_nope") 44 | 45 | 46 | def test_trailing_underscore_stripped(): 47 | """Trailing underscores are stripped when converting attribute names. 48 | This allows, for example, python keywords to function as SwaggerDict attributes.""" 49 | sd = openapi.SwaggerDict(trailing_underscore_="trailing") 50 | sd.in_ = "trailing" 51 | assert "in" in sd 52 | assert "trailingUnderscore" in sd 53 | assert sd.trailing_underscore == sd["in"] 54 | assert hasattr(sd, "in___") 55 | 56 | del sd.in_ 57 | assert "in" not in sd 58 | assert not hasattr(sd, "in__") 59 | 60 | 61 | def test_extra_ordering(): 62 | """Insertion order should also be consistent when setting undeclared parameters 63 | (kwargs) in SwaggerDict""" 64 | extras = [("beta", 1), ("alpha", 2), ("omega", 3), ("gamma", 4)] 65 | shuffled_extras = list(extras) 66 | shuffle(shuffled_extras) 67 | 68 | s1 = openapi.SwaggerDict(**OrderedDict(extras)) 69 | s2 = openapi.SwaggerDict(**OrderedDict(shuffled_extras)) 70 | 71 | assert list(s1.items()) == list(s2.items()) 72 | -------------------------------------------------------------------------------- /tests/test_versioning.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from drf_yasg.codecs import yaml_sane_load 4 | from drf_yasg.errors import SwaggerGenerationError 5 | 6 | 7 | def _get_versioned_schema(prefix, client, validate_schema, path="/snippets/"): 8 | response = client.get(prefix + "/swagger.yaml") 9 | assert response.status_code == 200 10 | swagger = yaml_sane_load(response.content.decode("utf-8")) 11 | _check_base(swagger, prefix, validate_schema, path) 12 | return swagger 13 | 14 | 15 | def _get_versioned_schema_management( 16 | prefix, call_generate_swagger, validate_schema, kwargs, path="/snippets/" 17 | ): 18 | output = call_generate_swagger( 19 | format="yaml", api_url="http://localhost" + prefix + "/swagger.yaml", **kwargs 20 | ) 21 | swagger = yaml_sane_load(output) 22 | _check_base(swagger, prefix, validate_schema, path) 23 | return swagger 24 | 25 | 26 | def _check_base(swagger, prefix, validate_schema, path): 27 | assert swagger["basePath"] == prefix 28 | validate_schema(swagger) 29 | assert path in swagger["paths"] 30 | return swagger 31 | 32 | 33 | def _check_v1(swagger, path="/snippets/"): 34 | assert swagger["info"]["version"] == "1.0" 35 | versioned_post = swagger["paths"][path]["post"] 36 | assert ( 37 | versioned_post["responses"]["201"]["schema"]["$ref"] == "#/definitions/Snippet" 38 | ) 39 | assert "v2field" not in swagger["definitions"]["Snippet"]["properties"] 40 | assert "/snippets_excluded/" not in swagger["paths"] 41 | 42 | 43 | def _check_v2(swagger, path="/snippets/"): 44 | assert swagger["info"]["version"] == "2.0" 45 | versioned_post = swagger["paths"][path]["post"] 46 | assert ( 47 | versioned_post["responses"]["201"]["schema"]["$ref"] 48 | == "#/definitions/SnippetV2" 49 | ) 50 | assert "v2field" in swagger["definitions"]["SnippetV2"]["properties"] 51 | v2field = swagger["definitions"]["SnippetV2"]["properties"]["v2field"] 52 | assert v2field["description"] == "version 2.0 field" 53 | assert "/snippets_excluded/" not in swagger["paths"] 54 | 55 | 56 | @pytest.mark.urls("urlconfs.url_versioning") 57 | def test_url_v2(client, validate_schema): 58 | swagger = _get_versioned_schema("/versioned/url/v2.0", client, validate_schema) 59 | _check_v2(swagger) 60 | 61 | 62 | @pytest.mark.urls("urlconfs.url_versioning_extra") 63 | def test_url_v2_extra(client, validate_schema): 64 | swagger = _get_versioned_schema( 65 | "/versioned/url/v2.0", client, validate_schema, path="/extra/snippets/" 66 | ) 67 | _check_v2(swagger, path="/extra/snippets/") 68 | 69 | 70 | @pytest.mark.urls("urlconfs.overrided_serializer_name") 71 | def test_url_same_ref_names(client, validate_schema): 72 | for v in {"v1.0", "v2.0"}: 73 | try: 74 | client.get(f"/versioned/url/{v}/swagger.yaml") 75 | except SwaggerGenerationError: 76 | pass 77 | else: 78 | raise AssertionError("drf_yasg.errors.SwaggerGenerationError is not raised") 79 | 80 | 81 | @pytest.mark.urls("urlconfs.url_versioning") 82 | def test_url_v1(client, validate_schema): 83 | swagger = _get_versioned_schema("/versioned/url/v1.0", client, validate_schema) 84 | _check_v1(swagger) 85 | 86 | 87 | @pytest.mark.urls("urlconfs.ns_versioning") 88 | def test_ns_v1(client, validate_schema): 89 | swagger = _get_versioned_schema("/versioned/ns/v1.0", client, validate_schema) 90 | _check_v1(swagger) 91 | 92 | 93 | @pytest.mark.urls("urlconfs.ns_versioning") 94 | def test_ns_v2(client, validate_schema): 95 | swagger = _get_versioned_schema("/versioned/ns/v2.0", client, validate_schema) 96 | _check_v2(swagger) 97 | 98 | 99 | @pytest.mark.urls("urlconfs.url_versioning") 100 | def test_url_v2_management(call_generate_swagger, validate_schema): 101 | kwargs = {"api_version": "2.0"} 102 | swagger = _get_versioned_schema_management( 103 | "/versioned/url/v2.0", call_generate_swagger, validate_schema, kwargs 104 | ) 105 | _check_v2(swagger) 106 | 107 | 108 | @pytest.mark.urls("urlconfs.url_versioning_extra") 109 | def test_url_v2_management_extra(call_generate_swagger, validate_schema): 110 | kwargs = {"api_version": "2.0"} 111 | swagger = _get_versioned_schema_management( 112 | "/versioned/url/v2.0", 113 | call_generate_swagger, 114 | validate_schema, 115 | kwargs, 116 | path="/extra/snippets/", 117 | ) 118 | _check_v2(swagger, path="/extra/snippets/") 119 | 120 | 121 | @pytest.mark.urls("urlconfs.ns_versioning") 122 | def test_ns_v2_management(call_generate_swagger, validate_schema): 123 | kwargs = {"api_version": "2.0"} 124 | swagger = _get_versioned_schema_management( 125 | "/versioned/ns/v2.0", call_generate_swagger, validate_schema, kwargs 126 | ) 127 | _check_v2(swagger) 128 | -------------------------------------------------------------------------------- /tests/urlconfs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axnsan12/drf-yasg/a4ced8d51404be7b5ca04268f955c6898496d37f/tests/urlconfs/__init__.py -------------------------------------------------------------------------------- /tests/urlconfs/additional_fields_checks.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from rest_framework import serializers 3 | 4 | from testproj.urls import required_urlpatterns 5 | 6 | from .url_versioning import ( 7 | VERSION_PREFIX_URL, 8 | SnippetList, 9 | SnippetSerializer, 10 | VersionedSchemaView, 11 | ) 12 | 13 | 14 | class SnippetsSerializer(serializers.HyperlinkedModelSerializer, SnippetSerializer): 15 | ipv4 = serializers.IPAddressField(required=False) 16 | uri = serializers.URLField(required=False) 17 | tracks = serializers.RelatedField( 18 | read_only=True, 19 | allow_null=True, 20 | allow_empty=True, 21 | many=True, 22 | ) 23 | 24 | class Meta: 25 | fields = tuple(SnippetSerializer().fields.keys()) + ( 26 | "ipv4", 27 | "uri", 28 | "tracks", 29 | "url", 30 | ) 31 | model = SnippetList.queryset.model 32 | 33 | 34 | class SnippetsV2Serializer(SnippetSerializer): 35 | url = serializers.HyperlinkedRelatedField( 36 | view_name="snippets-detail", source="*", read_only=True 37 | ) 38 | other_owner_snippets = serializers.PrimaryKeyRelatedField( 39 | read_only=True, source="owner.snippets", many=True 40 | ) 41 | owner_snippets = serializers.PrimaryKeyRelatedField(read_only=True, many=True) 42 | 43 | 44 | class SnippetsV1(SnippetList): 45 | serializer_class = SnippetsSerializer 46 | 47 | def get_serializer_class(self): 48 | return self.serializer_class 49 | 50 | 51 | class SnippetsV2(SnippetsV1): 52 | serializer_class = SnippetsV2Serializer 53 | 54 | 55 | urlpatterns = required_urlpatterns + [ 56 | re_path(VERSION_PREFIX_URL + r"snippets/$", SnippetsV1.as_view()), 57 | re_path(VERSION_PREFIX_URL + r"other_snippets/$", SnippetsV2.as_view()), 58 | re_path( 59 | VERSION_PREFIX_URL + r"swagger(?P.json|.yaml)$", 60 | VersionedSchemaView.without_ui(), 61 | name="vschema-json", 62 | ), 63 | ] 64 | -------------------------------------------------------------------------------- /tests/urlconfs/coreschema.py: -------------------------------------------------------------------------------- 1 | import coreapi 2 | import coreschema 3 | from django.urls import re_path 4 | from rest_framework import pagination 5 | 6 | from testproj.urls import required_urlpatterns 7 | 8 | from .url_versioning import VERSION_PREFIX_URL, SnippetList, VersionedSchemaView 9 | 10 | 11 | class FilterBackendWithoutParams: 12 | def filter_queryset(self, request, queryset, view): 13 | return queryset 14 | 15 | 16 | class OldFilterBackend(FilterBackendWithoutParams): 17 | def get_schema_fields(self, view): 18 | return [ 19 | coreapi.Field( 20 | name="test_param", 21 | required=False, 22 | location="query", 23 | schema=coreschema.String(title="Test", description="Test description"), 24 | ) 25 | ] 26 | 27 | 28 | class PaginatorV1(pagination.LimitOffsetPagination): 29 | def get_paginated_response_schema(self, schema): 30 | response_schema = super().get_paginated_response_schema(schema) 31 | del response_schema["properties"]["count"] 32 | return response_schema 33 | 34 | 35 | class PaginatorV2(pagination.LimitOffsetPagination): 36 | def __getattribute__(self, item): 37 | if item in {"get_paginated_response_schema", "get_schema_operation_parameters"}: 38 | raise AttributeError 39 | return super().__getattribute__(item) 40 | 41 | 42 | class PaginatorV3(PaginatorV1): 43 | def get_paginated_response_schema(self, schema): 44 | response_schema = super().get_paginated_response_schema(schema) 45 | response_schema["required"] = ["results"] 46 | return response_schema 47 | 48 | def __getattribute__(self, item): 49 | if item == "get_schema_fields": 50 | raise AttributeError() 51 | return super().__getattribute__(item) 52 | 53 | 54 | class SnippetsV1(SnippetList): 55 | filter_backends = list(SnippetList.filter_backends) + [ 56 | FilterBackendWithoutParams, 57 | OldFilterBackend, 58 | ] 59 | pagination_class = PaginatorV1 60 | 61 | 62 | class SnippetsV2(SnippetList): 63 | pagination_class = PaginatorV2 64 | 65 | 66 | class SnippetsV3(SnippetList): 67 | pagination_class = PaginatorV3 68 | 69 | 70 | urlpatterns = required_urlpatterns + [ 71 | re_path(VERSION_PREFIX_URL + r"snippets/$", SnippetsV1.as_view()), 72 | re_path(VERSION_PREFIX_URL + r"other_snippets/$", SnippetsV2.as_view()), 73 | re_path(VERSION_PREFIX_URL + r"ya_snippets/$", SnippetsV3.as_view()), 74 | re_path( 75 | VERSION_PREFIX_URL + r"swagger(?P.json|.yaml)$", 76 | VersionedSchemaView.without_ui(), 77 | name="vschema-json", 78 | ), 79 | ] 80 | -------------------------------------------------------------------------------- /tests/urlconfs/legacy_renderer.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from testproj.urls import SchemaView, required_urlpatterns 4 | 5 | urlpatterns = [ 6 | re_path( 7 | r"^swagger(?P\.json|\.yaml)$", 8 | SchemaView.without_ui(cache_timeout=0), 9 | name="schema-json", 10 | ), 11 | path( 12 | "swagger/", 13 | SchemaView.with_ui("swagger", cache_timeout=0), 14 | name="schema-swagger-ui", 15 | ), 16 | path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), 17 | path( 18 | "redoc-old/", 19 | SchemaView.with_ui("redoc-old", cache_timeout=0), 20 | name="schema-redoc-old", 21 | ), 22 | ] + required_urlpatterns 23 | -------------------------------------------------------------------------------- /tests/urlconfs/login_test_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from testproj.urls import required_urlpatterns 4 | 5 | 6 | def dummy(request): 7 | pass 8 | 9 | 10 | urlpatterns = required_urlpatterns + [ 11 | path("test/login", dummy, name="login"), 12 | path("test/logout", dummy, name="logout"), 13 | ] 14 | -------------------------------------------------------------------------------- /tests/urlconfs/non_public_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import permissions 3 | 4 | import testproj.urls 5 | from drf_yasg import openapi 6 | from drf_yasg.views import get_schema_view 7 | 8 | view = get_schema_view( 9 | openapi.Info("bla", "ble"), public=False, permission_classes=(permissions.AllowAny,) 10 | ) 11 | view = view.without_ui(cache_timeout=None) 12 | 13 | urlpatterns = [ 14 | path("", include(testproj.urls)), 15 | path("private/swagger.yaml", view, name="schema-private"), 16 | ] 17 | -------------------------------------------------------------------------------- /tests/urlconfs/ns_version1.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import generics, versioning 3 | 4 | from snippets.models import Snippet 5 | from snippets.serializers import SnippetSerializer 6 | from testproj.urls import required_urlpatterns 7 | 8 | 9 | class SnippetList(generics.ListCreateAPIView): 10 | """SnippetList classdoc""" 11 | 12 | queryset = Snippet.objects.all() 13 | serializer_class = SnippetSerializer 14 | versioning_class = versioning.NamespaceVersioning 15 | 16 | def perform_create(self, serializer): 17 | serializer.save(owner=self.request.user) 18 | 19 | def post(self, request, *args, **kwargs): 20 | """post method docstring""" 21 | return super(SnippetList, self).post(request, *args, **kwargs) 22 | 23 | 24 | app_name = "test_ns_versioning" 25 | 26 | urlpatterns = required_urlpatterns + [path("", SnippetList.as_view())] 27 | -------------------------------------------------------------------------------- /tests/urlconfs/ns_version2.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework import fields 3 | 4 | from snippets.serializers import SnippetSerializer 5 | from testproj.urls import required_urlpatterns 6 | 7 | from .ns_version1 import SnippetList as SnippetListV1 8 | 9 | 10 | class SnippetSerializerV2(SnippetSerializer): 11 | v2field = fields.IntegerField(help_text="version 2.0 field") 12 | 13 | class Meta: 14 | ref_name = "SnippetV2" 15 | 16 | 17 | class SnippetListV2(SnippetListV1): 18 | serializer_class = SnippetSerializerV2 19 | 20 | 21 | app_name = "2.0" 22 | 23 | urlpatterns = required_urlpatterns + [path("", SnippetListV2.as_view())] 24 | -------------------------------------------------------------------------------- /tests/urlconfs/ns_versioning.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path, re_path 2 | from rest_framework import versioning 3 | 4 | from testproj.urls import SchemaView, required_urlpatterns 5 | 6 | from . import ns_version1, ns_version2 7 | 8 | VERSION_PREFIX_NS = r"versioned/ns/" 9 | 10 | 11 | class VersionedSchemaView(SchemaView): 12 | versioning_class = versioning.NamespaceVersioning 13 | 14 | 15 | schema_patterns = [ 16 | re_path( 17 | r"swagger\.(?Pjson|yaml)$", 18 | VersionedSchemaView.without_ui(), 19 | name="ns-schema", 20 | ) 21 | ] 22 | 23 | 24 | urlpatterns = required_urlpatterns + [ 25 | path(VERSION_PREFIX_NS + "v1.0/snippets/", include(ns_version1, namespace="1.0")), 26 | path(VERSION_PREFIX_NS + "v2.0/snippets/", include(ns_version2)), 27 | path(VERSION_PREFIX_NS + "v1.0/", include((schema_patterns, "1.0"))), 28 | path(VERSION_PREFIX_NS + "v2.0/", include((schema_patterns, "2.0"))), 29 | ] 30 | -------------------------------------------------------------------------------- /tests/urlconfs/overrided_serializer_name.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from rest_framework import fields, generics, versioning 3 | 4 | from snippets.models import Snippet 5 | from snippets.serializers import SnippetSerializer 6 | from testproj.urls import SchemaView, required_urlpatterns 7 | 8 | 9 | class SnippetV1Serializer(SnippetSerializer): 10 | v1field = fields.IntegerField(help_text="version 1.0 field") 11 | 12 | 13 | class SnippetSerializerV2(SnippetV1Serializer): 14 | v2field = fields.IntegerField(help_text="version 2.0 field") 15 | 16 | class Meta: 17 | # Same name for check failing 18 | ref_name = "SnippetV1" 19 | 20 | 21 | class SnippetList(generics.ListCreateAPIView): 22 | """SnippetList classdoc""" 23 | 24 | queryset = Snippet.objects.all() 25 | serializer_class = SnippetV1Serializer 26 | versioning_class = versioning.URLPathVersioning 27 | 28 | def perform_create(self, serializer): 29 | serializer.save(owner=self.request.user) 30 | 31 | def post(self, request, *args, **kwargs): 32 | """post method docstring""" 33 | return super(SnippetList, self).post(request, *args, **kwargs) 34 | 35 | 36 | class SnippetsV2(SnippetList): 37 | serializer_class = SnippetSerializerV2 38 | 39 | 40 | VERSION_PREFIX_URL = r"^versioned/url/v(?P1.0|2.0)/" 41 | 42 | 43 | class VersionedSchemaView(SchemaView): 44 | versioning_class = versioning.URLPathVersioning 45 | 46 | 47 | urlpatterns = required_urlpatterns + [ 48 | re_path(VERSION_PREFIX_URL + r"snippets/$", SnippetList.as_view()), 49 | re_path(VERSION_PREFIX_URL + r"other_snippets/$", SnippetsV2.as_view()), 50 | re_path( 51 | VERSION_PREFIX_URL + r"swagger(?P.json|.yaml)$", 52 | VersionedSchemaView.without_ui(), 53 | name="vschema-json", 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /tests/urlconfs/url_versioning.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from rest_framework import fields, generics, versioning 3 | 4 | from snippets.models import Snippet 5 | from snippets.serializers import SnippetSerializer 6 | from testproj.urls import SchemaView, required_urlpatterns 7 | 8 | 9 | class SnippetSerializerV2(SnippetSerializer): 10 | v2field = fields.IntegerField(help_text="version 2.0 field") 11 | 12 | class Meta: 13 | ref_name = "SnippetV2" 14 | 15 | 16 | class SnippetList(generics.ListCreateAPIView): 17 | """SnippetList classdoc""" 18 | 19 | queryset = Snippet.objects.all() 20 | serializer_class = SnippetSerializer 21 | versioning_class = versioning.URLPathVersioning 22 | 23 | def get_serializer_class(self): 24 | context = self.get_serializer_context() 25 | request = context["request"] 26 | if int(float(request.version)) >= 2: 27 | return SnippetSerializerV2 28 | else: 29 | return SnippetSerializer 30 | 31 | def perform_create(self, serializer): 32 | serializer.save(owner=self.request.user) 33 | 34 | def post(self, request, *args, **kwargs): 35 | """post method docstring""" 36 | return super(SnippetList, self).post(request, *args, **kwargs) 37 | 38 | 39 | class ExcludedSnippets(SnippetList): 40 | swagger_schema = None 41 | 42 | 43 | VERSION_PREFIX_URL = r"^versioned/url/v(?P1.0|2.0)/" 44 | 45 | 46 | class VersionedSchemaView(SchemaView): 47 | versioning_class = versioning.URLPathVersioning 48 | 49 | 50 | urlpatterns = required_urlpatterns + [ 51 | re_path(VERSION_PREFIX_URL + r"snippets/$", SnippetList.as_view()), 52 | re_path(VERSION_PREFIX_URL + r"snippets_excluded/$", ExcludedSnippets.as_view()), 53 | re_path( 54 | VERSION_PREFIX_URL + r"swagger\.(?Pjson|yaml)$", 55 | VersionedSchemaView.without_ui(), 56 | name="vschema-json", 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /tests/urlconfs/url_versioning_extra.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from testproj.urls import required_urlpatterns 4 | 5 | from .url_versioning import VERSION_PREFIX_URL, SnippetList, VersionedSchemaView 6 | 7 | urlpatterns = required_urlpatterns + [ 8 | re_path(VERSION_PREFIX_URL + r"extra/snippets/$", SnippetList.as_view()), 9 | re_path(VERSION_PREFIX_URL + r"extra2/snippets/$", SnippetList.as_view()), 10 | re_path( 11 | VERSION_PREFIX_URL + r"swagger(?P.json|.yaml)$", 12 | VersionedSchemaView.without_ui(), 13 | name="vschema-json", 14 | ), 15 | ] 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.3.0 3 | isolated_build = true 4 | isolated_build_env = .package 5 | 6 | # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django 7 | envlist = 8 | py{37,38,39}-django{22,30}-drf{310,311,312}, 9 | py{37,38,39}-django{31,32}-drf{311,312}, 10 | py{39,310}-django{40,41}-drf{313,314}, 11 | py311-django{40,41,42,50,51}-drf315, 12 | py312-django{42,50,51}-drf315, 13 | py313-django{51}-drf315, 14 | py38-{lint, docs}, 15 | # py313-djmaster # ModuleNotFoundError: No module named 'psycopg' 16 | 17 | skip_missing_interpreters = true 18 | 19 | [testenv:.package] 20 | # no additional dependencies besides PEP 517 21 | deps = 22 | 23 | [testenv:py310-djmaster] 24 | ignore_outcome = true 25 | 26 | [testenv] 27 | deps = 28 | django22: Django>=2.2,<2.3 29 | django30: Django>=3.0,<3.1 30 | django31: Django>=3.1,<3.2 31 | django32: Django>=3.2,<3.3 32 | django40: Django>=4.0,<4.1 33 | django41: Django>=4.1,<4.2 34 | django42: Django>=4.2,<5 35 | 36 | drf310: djangorestframework>=3.10,<3.11 37 | drf311: djangorestframework>=3.11,<3.12 38 | drf312: djangorestframework>=3.12,<3.13 39 | drf313: djangorestframework>=3.13,<3.14 40 | drf314: djangorestframework>=3.14,<3.15 41 | drf315: djangorestframework>=3.15,<3.16 42 | 43 | typing: typing>=3.6.6 44 | 45 | # test with the latest builds of Django and django-rest-framework 46 | # to get early warning of compatibility issues 47 | djmaster: https://github.com/django/django/archive/main.tar.gz 48 | djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz 49 | 50 | # other dependencies 51 | -r requirements/validation.txt 52 | -r requirements/test.txt 53 | drf310: coreapi>=2.3.3 54 | drf310: coreschema>=0.0.4 55 | drf310: flex~=6.14.1 56 | 57 | commands = 58 | pytest -n 2 --cov --cov-config .coveragerc --cov-append --cov-report="" {posargs} 59 | 60 | [testenv:py38-lint] 61 | skip_install = true 62 | deps = 63 | -r requirements/lint.txt 64 | commands = 65 | ruff check 66 | ruff format --check 67 | 68 | [testenv:py38-docs] 69 | deps = 70 | -r requirements/docs.txt 71 | commands = 72 | sphinx-build -WnEa -b html docs docs/_build/html 73 | 74 | [pytest] 75 | DJANGO_SETTINGS_MODULE = testproj.settings.local 76 | python_paths = testproj 77 | addopts = --ignore=node_modules 78 | 79 | [flake8] 80 | max-line-length = 120 81 | exclude = **/migrations/* 82 | ignore = E721,F405,I001,I005,W504 83 | 84 | [isort] 85 | skip = .eggs,.tox,docs,env,venv,node_modules,.git 86 | skip_glob = **/migrations/* 87 | atomic = true 88 | multi_line_output = 5 89 | line_length = 120 90 | known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs 91 | -------------------------------------------------------------------------------- /update-ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | npm update 4 | 5 | cp node_modules/redoc/bundles/redoc.standalone.js src/drf_yasg/static/drf-yasg/redoc/redoc.min.js 6 | cp node_modules/redoc/bundles/redoc.standalone.js.map src/drf_yasg/static/drf-yasg/redoc/ 7 | cp node_modules/redoc/LICENSE src/drf_yasg/static/drf-yasg/redoc/LICENSE 8 | curl -o src/drf_yasg/static/drf-yasg/redoc-old/redoc.min.js https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js 9 | curl -o src/drf_yasg/static/drf-yasg/redoc-old/redoc.min.js.map https://rebilly.github.io/ReDoc/releases/v1.x.x/redoc.min.js.map 10 | curl -o src/drf_yasg/static/drf-yasg/redoc-old/LICENSE https://raw.githubusercontent.com/Redocly/redoc/v1.x/LICENSE 11 | 12 | cp -r node_modules/swagger-ui-dist src/drf_yasg/static/drf-yasg/ 13 | pushd src/drf_yasg/static/drf-yasg/swagger-ui-dist/ >/dev/null 14 | rm -f package.json .npmignore README.md favicon-16x16.png 15 | rm -f swagger-ui.js index.html 16 | popd >/dev/null 17 | --------------------------------------------------------------------------------