├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTORS.txt ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── make.bat ├── requirements.txt ├── requirements_extra.txt └── source │ ├── changelog.rst │ ├── conf.py │ ├── index.rst │ └── users │ ├── installation.rst │ └── oauth2_settings.rst ├── manage.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini └── src └── wagtail_oauth2 ├── __init__.py ├── resources.py ├── settings.py ├── templates └── login_error.html ├── tests ├── __init__.py ├── apps.py ├── conftest.py ├── settings.py ├── test_resources.py ├── test_utils.py ├── test_views.py └── urls.py ├── urls.py ├── utils.py └── views.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | src/wagtail_oauth2/tests/* -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.py] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | [*.rst] 6 | indent_style = space 7 | indent_size = 3 8 | 9 | [*.toml] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.7, 3.8, 3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install poetry 25 | run: pip install poetry 26 | - name: Install wagtail-oauth2 27 | run: poetry install 28 | - name: Run Tests 29 | run: | 30 | poetry run pytest --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=wagtail_oauth2 --cov-report=xml --cov-report=html 31 | 32 | - name: Upload pytest test results 33 | uses: actions/upload-artifact@v2 34 | with: 35 | name: pytest-results-${{ matrix.python-version }} 36 | path: junit/test-results-${{ matrix.python-version }}.xml 37 | 38 | - name: Codecov 39 | uses: codecov/codecov-action@v2.1.0 40 | with: 41 | # Comma-separated list of files to upload 42 | files: coverage.xml 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .DS_Store 4 | *.swp 5 | .idea 6 | .vscode 7 | coverage 8 | 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | *.egg-info/ 17 | *.egg 18 | *.db 19 | 20 | .coverage 21 | tests/*.xml 22 | htmlcov 23 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 3 | 4 | # Set the version of Python and other tools you might need 5 | build: 6 | os: ubuntu-20.04 7 | tools: 8 | python: "3.9" 9 | # You can also specify other tool versions: 10 | # nodejs: "16" 11 | # rust: "1.55" 12 | # golang: "1.17" 13 | 14 | sphinx: 15 | configuration: 'docs/source/conf.py' 16 | fail_on_warning: true 17 | 18 | python: 19 | install: 20 | - requirements: 'docs/requirements.txt' 21 | - requirements: 'docs/requirements_extra.txt' 22 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.3.0 (2022-01-04) 2 | ------------------- 3 | * Add a way to retrieve and refresh an oauth2 token to do API Calls 4 | 5 | 0.2.1 (2021-12-09) 6 | ------------------- 7 | * Fix documentation link 8 | 9 | 0.2.0 (2021-12-09) 10 | ------------------- 11 | * Improve documentation 12 | * Add CI Workflow 13 | 14 | 0.1.1 (2022-12-09) 15 | ------------------- 16 | * Initial Release 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Guillaume Gauvrit 2 | Yann Brelière -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Gandi and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Gandi nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Wagtail OAuth2 3 | ============== 4 | 5 | .. image:: https://readthedocs.org/projects/wagtail-oauth2/badge/?version=latest 6 | :target: https://wagtail-oauth2.readthedocs.io/en/latest/?badge=latest 7 | :alt: Documentation Status 8 | 9 | .. image:: https://github.com/Gandi/wagtail-oauth2/actions/workflows/main.yml/badge.svg 10 | :target: https://github.com/Gandi/wagtail-oauth2/actions/workflows/main.yml 11 | :alt: Build Status 12 | 13 | 14 | .. image:: https://codecov.io/gh/Gandi/wagtail-oauth2/branch/main/graph/badge.svg?token=VN14GVV3Y0 15 | :target: https://codecov.io/gh/Gandi/wagtail-oauth2 16 | :alt: Coverage 17 | 18 | 19 | Plugin to replace Wagtail default login by an OAuth2.0 Authorization Server. 20 | 21 | What is wagtail-oauth2 22 | ---------------------- 23 | 24 | OAuth2.0 is an authorization framework widely used and usually, 25 | OAuth2.0 authorization servers grant authorization on authenticated user. 26 | 27 | 28 | The OAuth2 Authorization is used as an **identity provider**. 29 | 30 | 31 | Read More 32 | --------- 33 | 34 | You can read the `full documentation of this library here`_. 35 | 36 | 37 | .. _`full documentation of this library here`: https://wagtail-oauth2.readthedocs.io/en/latest/ -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 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/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=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 2 | --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ 3 | --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 4 | anyascii==0.3.0; python_version >= "3.6" \ 5 | --hash=sha256:68f6917fe5b22caf7dde8551b838e5e17d5e3c96c55734485699bd03ad92237f \ 6 | --hash=sha256:24f27431fb64c6c93a33125fb66f8cba007a5262bc1faabeafeda5f4bb70b593 7 | asgiref==3.4.1; python_version >= "3.6" \ 8 | --hash=sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214 \ 9 | --hash=sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9 10 | atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" \ 11 | --hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \ 12 | --hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a 13 | attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 14 | --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ 15 | --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb 16 | babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 17 | --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ 18 | --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 19 | beautifulsoup4==4.9.3; python_version >= "3.6" \ 20 | --hash=sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35 \ 21 | --hash=sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666 \ 22 | --hash=sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25 23 | black==21.12b0; python_full_version >= "3.6.2" \ 24 | --hash=sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f \ 25 | --hash=sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3 26 | certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" \ 27 | --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 \ 28 | --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 29 | charset-normalizer==2.0.9; python_full_version >= "3.6.0" and python_version >= "3.6" \ 30 | --hash=sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c \ 31 | --hash=sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721 32 | click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" \ 33 | --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \ 34 | --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b 35 | colorama==0.4.4; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0") and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") and python_full_version >= "3.5.0") \ 36 | --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \ 37 | --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b 38 | coverage==6.2; python_version >= "3.6" \ 39 | --hash=sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b \ 40 | --hash=sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0 \ 41 | --hash=sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da \ 42 | --hash=sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d \ 43 | --hash=sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739 \ 44 | --hash=sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971 \ 45 | --hash=sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840 \ 46 | --hash=sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c \ 47 | --hash=sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f \ 48 | --hash=sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76 \ 49 | --hash=sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47 \ 50 | --hash=sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64 \ 51 | --hash=sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9 \ 52 | --hash=sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d \ 53 | --hash=sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48 \ 54 | --hash=sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e \ 55 | --hash=sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d \ 56 | --hash=sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17 \ 57 | --hash=sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781 \ 58 | --hash=sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a \ 59 | --hash=sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0 \ 60 | --hash=sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49 \ 61 | --hash=sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521 \ 62 | --hash=sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884 \ 63 | --hash=sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa \ 64 | --hash=sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64 \ 65 | --hash=sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617 \ 66 | --hash=sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8 \ 67 | --hash=sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4 \ 68 | --hash=sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74 \ 69 | --hash=sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e \ 70 | --hash=sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58 \ 71 | --hash=sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc \ 72 | --hash=sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd \ 73 | --hash=sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953 \ 74 | --hash=sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475 \ 75 | --hash=sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57 \ 76 | --hash=sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c \ 77 | --hash=sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2 \ 78 | --hash=sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd \ 79 | --hash=sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685 \ 80 | --hash=sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c \ 81 | --hash=sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3 \ 82 | --hash=sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282 \ 83 | --hash=sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644 \ 84 | --hash=sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de \ 85 | --hash=sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8 86 | django-filter==21.1; python_version >= "3.6" \ 87 | --hash=sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e \ 88 | --hash=sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063 89 | django-modelcluster==5.2; python_version >= "3.6" \ 90 | --hash=sha256:e541a46a0a899ef4778a4708be22e71cac3efacc09a6ff44bc065c5c9194c054 \ 91 | --hash=sha256:767084078b9e172540b271454ecc73cb320927131ca4b2c5f276daf771f9542f 92 | django-taggit==1.5.1; python_version >= "3.6" \ 93 | --hash=sha256:e5bb62891f458d55332e36a32e19c08d20142c43f74bc5656c803f8af25c084a \ 94 | --hash=sha256:dfe9e9c10b5929132041de0c00093ef0072c73c2a97d0f74a818ae50fa77149a 95 | django-treebeard==4.5.1; python_version >= "3.6" \ 96 | --hash=sha256:80150017725239702054e5fa64dc66e383dc13ac262c8d47ee5a82cb005969da \ 97 | --hash=sha256:7c2b1cdb1e9b46d595825186064a1228bc4d00dbbc186db5b0b9412357fba91c 98 | django==3.2.10; python_version >= "3.6" \ 99 | --hash=sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3 \ 100 | --hash=sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4 101 | djangorestframework==3.12.4; python_version >= "3.6" \ 102 | --hash=sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf \ 103 | --hash=sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2 104 | docutils==0.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 105 | --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 \ 106 | --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 107 | draftjs-exporter==2.1.7; python_version >= "3.6" \ 108 | --hash=sha256:d415a9964690a2cddb66a31ef32dd46c277e9b80434b94e39e3043188ed83e33 \ 109 | --hash=sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb 110 | et-xmlfile==1.1.0; python_version >= "3.6" \ 111 | --hash=sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada \ 112 | --hash=sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c 113 | faker==10.0.0; python_version >= "3.6" \ 114 | --hash=sha256:3163c84866cf118ac5329a802e046b0f729528ce62ebb2806b626e0badbb6ff3 \ 115 | --hash=sha256:530690ad12a2a054071af95fc8a354c5fd57b5e7707053a9662f40f14a87b68e 116 | html5lib==1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 117 | --hash=sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d \ 118 | --hash=sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f 119 | idna==3.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" \ 120 | --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ 121 | --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d 122 | imagesize==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 123 | --hash=sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c \ 124 | --hash=sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d 125 | importlib-metadata==4.8.2; python_version >= "3.6" and python_full_version >= "3.6.2" and python_version < "3.8" \ 126 | --hash=sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100 \ 127 | --hash=sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb 128 | iniconfig==1.1.1; python_version >= "3.6" \ 129 | --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ 130 | --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 131 | isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" \ 132 | --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \ 133 | --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951 134 | jinja2==3.0.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 135 | --hash=sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 \ 136 | --hash=sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7 137 | l18n==2021.3; python_version >= "3.6" \ 138 | --hash=sha256:78495d1df95b6f7dcc694d1ba8994df709c463a1cbac1bf016e1b9a5ce7280b9 \ 139 | --hash=sha256:1956e890d673d17135cc20913253c154f6bc1c00266c22b7d503cc1a5a42d848 140 | markupsafe==2.0.1; python_version >= "3.6" \ 141 | --hash=sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53 \ 142 | --hash=sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38 \ 143 | --hash=sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad \ 144 | --hash=sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d \ 145 | --hash=sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646 \ 146 | --hash=sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b \ 147 | --hash=sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a \ 148 | --hash=sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a \ 149 | --hash=sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28 \ 150 | --hash=sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134 \ 151 | --hash=sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51 \ 152 | --hash=sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff \ 153 | --hash=sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b \ 154 | --hash=sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94 \ 155 | --hash=sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872 \ 156 | --hash=sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f \ 157 | --hash=sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c \ 158 | --hash=sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724 \ 159 | --hash=sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145 \ 160 | --hash=sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd \ 161 | --hash=sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f \ 162 | --hash=sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6 \ 163 | --hash=sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d \ 164 | --hash=sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9 \ 165 | --hash=sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567 \ 166 | --hash=sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18 \ 167 | --hash=sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f \ 168 | --hash=sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f \ 169 | --hash=sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2 \ 170 | --hash=sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d \ 171 | --hash=sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85 \ 172 | --hash=sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6 \ 173 | --hash=sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864 \ 174 | --hash=sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207 \ 175 | --hash=sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9 \ 176 | --hash=sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86 \ 177 | --hash=sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415 \ 178 | --hash=sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914 \ 179 | --hash=sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9 \ 180 | --hash=sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066 \ 181 | --hash=sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35 \ 182 | --hash=sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b \ 183 | --hash=sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298 \ 184 | --hash=sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75 \ 185 | --hash=sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb \ 186 | --hash=sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b \ 187 | --hash=sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a \ 188 | --hash=sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6 \ 189 | --hash=sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f \ 190 | --hash=sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194 \ 191 | --hash=sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee \ 192 | --hash=sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64 \ 193 | --hash=sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833 \ 194 | --hash=sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26 \ 195 | --hash=sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7 \ 196 | --hash=sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8 \ 197 | --hash=sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5 \ 198 | --hash=sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135 \ 199 | --hash=sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902 \ 200 | --hash=sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509 \ 201 | --hash=sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1 \ 202 | --hash=sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac \ 203 | --hash=sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6 \ 204 | --hash=sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047 \ 205 | --hash=sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e \ 206 | --hash=sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1 \ 207 | --hash=sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74 \ 208 | --hash=sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8 \ 209 | --hash=sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a 210 | mypy-extensions==0.4.3; python_full_version >= "3.6.2" \ 211 | --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ 212 | --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 213 | openpyxl==3.0.9; python_version >= "3.6" \ 214 | --hash=sha256:8f3b11bd896a95468a4ab162fc4fcd260d46157155d1f8bfaabb99d88cfcf79f \ 215 | --hash=sha256:40f568b9829bf9e446acfffce30250ac1fa39035124d55fc024025c41481c90f 216 | packaging==21.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 217 | --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 \ 218 | --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb 219 | pathspec==0.9.0; python_full_version >= "3.6.2" \ 220 | --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ 221 | --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 222 | pillow==8.4.0; python_version >= "3.6" \ 223 | --hash=sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d \ 224 | --hash=sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6 \ 225 | --hash=sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78 \ 226 | --hash=sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649 \ 227 | --hash=sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f \ 228 | --hash=sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a \ 229 | --hash=sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39 \ 230 | --hash=sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55 \ 231 | --hash=sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c \ 232 | --hash=sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a \ 233 | --hash=sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645 \ 234 | --hash=sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9 \ 235 | --hash=sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff \ 236 | --hash=sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153 \ 237 | --hash=sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29 \ 238 | --hash=sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8 \ 239 | --hash=sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488 \ 240 | --hash=sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b \ 241 | --hash=sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b \ 242 | --hash=sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49 \ 243 | --hash=sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585 \ 244 | --hash=sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779 \ 245 | --hash=sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409 \ 246 | --hash=sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df \ 247 | --hash=sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09 \ 248 | --hash=sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76 \ 249 | --hash=sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a \ 250 | --hash=sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e \ 251 | --hash=sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b \ 252 | --hash=sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20 \ 253 | --hash=sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed \ 254 | --hash=sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02 \ 255 | --hash=sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b \ 256 | --hash=sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2 \ 257 | --hash=sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad \ 258 | --hash=sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698 \ 259 | --hash=sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc \ 260 | --hash=sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df \ 261 | --hash=sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b \ 262 | --hash=sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc \ 263 | --hash=sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed 264 | platformdirs==2.4.0; python_version >= "3.6" and python_full_version >= "3.6.2" \ 265 | --hash=sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d \ 266 | --hash=sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2 267 | pluggy==1.0.0; python_version >= "3.6" \ 268 | --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 \ 269 | --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 270 | py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 271 | --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 \ 272 | --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 273 | pygments==2.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 274 | --hash=sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380 \ 275 | --hash=sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6 276 | pyparsing==3.0.6; python_version >= "3.6" \ 277 | --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \ 278 | --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81 279 | pytest-cov==3.0.0; python_version >= "3.6" \ 280 | --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 \ 281 | --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 282 | pytest-django==4.5.2; python_version >= "3.5" \ 283 | --hash=sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2 \ 284 | --hash=sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e 285 | pytest==6.2.5; python_version >= "3.6" \ 286 | --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 \ 287 | --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 288 | python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \ 289 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 290 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 291 | pytz==2021.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 292 | --hash=sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c \ 293 | --hash=sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326 294 | requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ 295 | --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ 296 | --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 297 | six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 298 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ 299 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 300 | snowballstemmer==2.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 301 | --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a \ 302 | --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 303 | soupsieve==2.3.1; python_version >= "3.6" \ 304 | --hash=sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb \ 305 | --hash=sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9 306 | sphinx-rtd-theme==1.0.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ 307 | --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ 308 | --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c 309 | sphinx==4.3.1; python_version >= "3.6" \ 310 | --hash=sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f \ 311 | --hash=sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45 312 | sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 313 | --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 \ 314 | --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a 315 | sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 316 | --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 \ 317 | --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e 318 | sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 319 | --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 \ 320 | --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 321 | sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 322 | --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 \ 323 | --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 324 | sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 325 | --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ 326 | --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 327 | sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ 328 | --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 \ 329 | --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd 330 | sqlparse==0.4.2; python_version >= "3.6" \ 331 | --hash=sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d \ 332 | --hash=sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae 333 | tablib==3.1.0; python_version >= "3.6" \ 334 | --hash=sha256:26141c9cf2d5904a2228d3f5d45f8a46a3f3f2f0fbb4c33b4a1c1ddca9f31348 \ 335 | --hash=sha256:d64c9f6712918a3d90ec5d71b44b8bab1083e3609e4844ad2be80eb633e097ed 336 | telepath==0.2; python_version >= "3.6" \ 337 | --hash=sha256:801615094d3d964e178183099bf04020f4ff9c84ec43945d40b096df0a5767ee \ 338 | --hash=sha256:ef4cf2a45ed1908c58639c346756955f8a73ae79002a8116d596b3fd702bf84c 339 | text-unidecode==1.3; python_version >= "3.6" \ 340 | --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 \ 341 | --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 342 | toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \ 343 | --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ 344 | --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f 345 | tomli==1.2.2; python_version >= "3.6" and python_full_version >= "3.6.2" \ 346 | --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade \ 347 | --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee 348 | typed-ast==1.5.1; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.6.2" and python_version >= "3.6" \ 349 | --hash=sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212 \ 350 | --hash=sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631 \ 351 | --hash=sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb \ 352 | --hash=sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08 \ 353 | --hash=sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e \ 354 | --hash=sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695 \ 355 | --hash=sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30 \ 356 | --hash=sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f \ 357 | --hash=sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471 \ 358 | --hash=sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb \ 359 | --hash=sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb \ 360 | --hash=sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af \ 361 | --hash=sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d \ 362 | --hash=sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a \ 363 | --hash=sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32 \ 364 | --hash=sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4 \ 365 | --hash=sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d \ 366 | --hash=sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775 \ 367 | --hash=sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5 368 | typing-extensions==4.0.1 \ 369 | --hash=sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b \ 370 | --hash=sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e 371 | urllib3==1.26.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6" \ 372 | --hash=sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844 \ 373 | --hash=sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece 374 | wagtail==2.15.1; python_version >= "3.6" \ 375 | --hash=sha256:6f1bc7960a7028a6e665a0f5a15ef2ba89faad66060bc4b3952a922fbef1713b \ 376 | --hash=sha256:97eb520db0056d576b698e832de2ecfa3d795e9ed12e4b9ab738623c1b6277a0 377 | webencodings==0.5.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ 378 | --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ 379 | --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 380 | willow==1.4; python_version >= "3.6" \ 381 | --hash=sha256:698f755fc6bfb8984ac8550f470a0cb630ec1e628287475315d4d1e7595d7337 \ 382 | --hash=sha256:cde01e054c510284ac3459d6b531e1653a58e33a735706ac27905a94fe81742c 383 | xlrd==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.6.0" \ 384 | --hash=sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd \ 385 | --hash=sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88 386 | xlsxwriter==3.0.2; python_version >= "3.6" \ 387 | --hash=sha256:1aa65166697c42284e82f5bf9a33c2e913341eeef2b262019c3f5b5334768765 \ 388 | --hash=sha256:53005f03e8eb58f061ebf41d5767c7495ee0772c2396fe26b7e0ca22fa9c2570 389 | xlwt==1.3.0; python_version >= "3.6" \ 390 | --hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \ 391 | --hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88 392 | xmlrunner==1.7.7 \ 393 | --hash=sha256:5a6113d049eca7646111ee657266966e5bbfb0b5ceb2e83ee0772e16d7110f39 394 | zipp==3.6.0; python_version >= "3.6" and python_full_version >= "3.6.2" and python_version < "3.8" \ 395 | --hash=sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc \ 396 | --hash=sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832 397 | -------------------------------------------------------------------------------- /docs/requirements_extra.txt: -------------------------------------------------------------------------------- 1 | sphinx==4.2.0 2 | sphinx_autodoc_typehints==1.12.0 3 | sphinx-rtd-theme==1.0.0 4 | tomlkit==0.7.2 -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. include:: ../../CHANGELOG.rst 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import tomlkit 16 | 17 | sys.path.insert(0, os.path.abspath("../../src")) 18 | 19 | # -- Project information ----------------------------------------------------- 20 | def _get_project_meta(): 21 | with open("../../pyproject.toml") as pyproject: 22 | file_contents = pyproject.read() 23 | 24 | return tomlkit.parse(file_contents)["tool"]["poetry"] 25 | 26 | 27 | project = 'Wagtail-OAuth2' 28 | copyright = '2021, Gandi' 29 | # author = 'Gandi' 30 | 31 | pkg_meta = _get_project_meta() 32 | project = str(pkg_meta["name"]) 33 | author = str(pkg_meta["authors"][0]) 34 | 35 | # The short X.Y version 36 | version = str(pkg_meta["version"]) 37 | # The full version, including alpha/beta/rc tags 38 | release = version 39 | 40 | # -- General configuration --------------------------------------------------- 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # List of patterns, relative to source directory, that match files and 52 | # directories to ignore when looking for source files. 53 | # This pattern also affects html_static_path and html_extra_path. 54 | exclude_patterns = [] 55 | 56 | 57 | # -- Options for HTML output ------------------------------------------------- 58 | 59 | # The theme to use for HTML and HTML Help pages. See the documentation for 60 | # a list of builtin themes. 61 | # 62 | html_theme = 'sphinx_rtd_theme' 63 | 64 | # Add any paths that contain custom static files (such as style sheets) here, 65 | # relative to this directory. They are copied after the builtin static files, 66 | # so a file named "default.css" will overwrite the builtin "default.css". 67 | # html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Wagtail-OAuth2 documentation master file, created by 2 | sphinx-quickstart on Thu Dec 9 08:54:19 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../../README.rst 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :caption: Users Documentation: 11 | 12 | users/installation 13 | users/oauth2_settings 14 | changelog 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/source/users/installation.rst: -------------------------------------------------------------------------------- 1 | Install and configure wagtail-oauth2 2 | ==================================== 3 | 4 | 5 | Installation 6 | ------------ 7 | 8 | `wagtail-oauth2` is available on pypi, you can install with your favorite 9 | packaging tool. 10 | 11 | .. note:: 12 | 13 | Example using pip 14 | 15 | :: 16 | 17 | pip install wagtail-oauth2 18 | 19 | Django's Configuration 20 | ---------------------- 21 | 22 | INSTALLED_APPS 23 | ~~~~~~~~~~~~~~ 24 | 25 | `wagtail-oauth2` is a Django application that must be installed, 26 | so it must be in the `INSTALLED_APPS` list of your settings module. 27 | And, to work, it requires other app that are probably already installed. 28 | 29 | Here is the lists of the minimum installed app. 30 | 31 | :: 32 | 33 | INSTALLED_APPS = [ 34 | # ... 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "wagtail_oauth2", 39 | "wagtail.admin", 40 | "wagtail.users", 41 | "wagtail.core", 42 | # ... 43 | ] 44 | 45 | 46 | MIDDLEWARE 47 | ~~~~~~~~~~ 48 | 49 | Every authentication system requires a session middleware, 50 | `wagtail-oauth2` does not escape this rule. 51 | 52 | :: 53 | 54 | MIDDLEWARE = [ 55 | "django.contrib.sessions.middleware.SessionMiddleware" 56 | ] 57 | 58 | 59 | TEMPLATES 60 | ~~~~~~~~~ 61 | 62 | `wagtail-oauth2` may render a template `login_error.html` if issues happen 63 | during the OAuth2.0 dance. 64 | 65 | The template is provided using django template system in the package, 66 | the settings `APP_DIRS` must be set to `True` in order to render it. 67 | 68 | :: 69 | 70 | TEMPLATES = [ 71 | { 72 | "BACKEND": "django.template.backends.django.DjangoTemplates", 73 | "APP_DIRS": True, 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /docs/source/users/oauth2_settings.rst: -------------------------------------------------------------------------------- 1 | Configure OAuth2.0 2 | ------------------ 3 | 4 | `wagtail-oauth2` requires a few settings to works. 5 | 6 | They are all prefixed by `OAUTH2_`: 7 | 8 | .. |br| raw:: html 9 | 10 |
11 | 12 | 13 | +---------------------------+---------------------------+---------------------+-----------+ 14 | | name | description | mandatory | type | 15 | +===========================+===========================+=====================+===========+ 16 | | OAUTH2_CLIENT_ID | OAuth2.0 client id | yes | str | 17 | +---------------------------+---------------------------+---------------------+-----------+ 18 | | OAUTH2_CLIENT_SECRET | OAuth2.0 client secret | yes | str | 19 | +---------------------------+---------------------------+---------------------+-----------+ 20 | | OAUTH2_AUTH_URL | /authorize endpoint | yes | str | 21 | +---------------------------+---------------------------+---------------------+-----------+ 22 | | OAUTH2_TOKEN_URL | /token url | yes | str | 23 | +---------------------------+---------------------------+---------------------+-----------+ 24 | | OAUTH2_LOAD_USERINFO | Load user info |br| | yes | callable | 25 | | | from the oauth2 server | | | 26 | +---------------------------+---------------------------+---------------------+-----------+ 27 | | OAUTH2_LOGOUT_URL | url to redirect on logout | yes | str | 28 | +---------------------------+---------------------------+---------------------+-----------+ 29 | | OAUTH2_TIMEOUT | HTTP Timeout in seconds | no (30) | int | 30 | +---------------------------+---------------------------+---------------------+-----------+ 31 | | OAUTH2_VERIFY_CERTIFICATE | Check TLS while |br| | no (True) | bool | 32 | | | consuming tokens | | | 33 | +---------------------------+---------------------------+---------------------+-----------+ 34 | | OAUTH2_STORE_TOKENS | Save the tokens |br| | no (False) | bool | 35 | | | in the django session | | | 36 | +---------------------------+---------------------------+---------------------+-----------+ 37 | | OAUTH2_SESSION_KEY_PREFIX | Prefix of the key in |br| | no | | 38 | | | the django session. | (`wagtail_oauth2_`) | str | 39 | +---------------------------+---------------------------+---------------------+-----------+ 40 | | OAUTH2_DEFAULT_TTL | Fallback value if |br| | no (900) | int | 41 | | | ``expires_in`` is | | | 42 | | | missing | | | 43 | +---------------------------+---------------------------+---------------------+-----------+ 44 | 45 | 46 | The settings `OAUTH2_LOAD_USERINFO` is a function that takes an `access_token` in parameter, 47 | and builds a python dict or raises a `PermissionDenied` error. 48 | 49 | Basically, this method is about fetching some information on the user loaded using 50 | OAuth2.0 API and deciding to grant the user to log in, and to get the role of 51 | that user. 52 | 53 | The userinfo dict contains the following keys: 54 | 55 | +--------------+-------------------------------------+--------------------------------+-----------+ 56 | | key | description | mandatory (default) | type | 57 | +==============+=====================================+================================+===========+ 58 | | username | user identifier | yes | str | 59 | +--------------+-------------------------------------+--------------------------------+-----------+ 60 | | is_superuser | makes the user an admin on creation | yes | bool | 61 | +--------------+-------------------------------------+--------------------------------+-----------+ 62 | | email | email address (recommanded) | no | str | 63 | +--------------+-------------------------------------+--------------------------------+-----------+ 64 | | first_name | first name of the user | no | str | 65 | +--------------+-------------------------------------+--------------------------------+-----------+ 66 | | last_name | last name of the user | no | str | 67 | +--------------+-------------------------------------+--------------------------------+-----------+ 68 | | is_staff | grant access to the wagtail admin | no (True) | bool | 69 | +--------------+-------------------------------------+--------------------------------+-----------+ 70 | | groups | subscribe non superuser to groups | no (["Moderators", "Editors"]) | List[str] | 71 | +--------------+-------------------------------------+--------------------------------+-----------+ 72 | 73 | 74 | Exemple of settings 75 | ~~~~~~~~~~~~~~~~~~~ 76 | 77 | :: 78 | 79 | 80 | USERS = { 81 | "mey_accesstoken": { 82 | "username": "mei", 83 | "is_superuser": True, 84 | } 85 | } 86 | 87 | 88 | def load_userinfo(access_token): 89 | try: 90 | # Real code consume an api with a header 91 | # f"Authorization: Bearer {access_token}" 92 | return USERS[access_token] 93 | except KeyError: 94 | raise PermissionDenied 95 | 96 | 97 | OAUTH2_LOAD_USERINFO = load_userinfo 98 | 99 | OAUTH2_CLIENT_ID = "Mei" 100 | OAUTH2_CLIENT_SECRET = "T0t0r0" 101 | 102 | OAUTH2_AUTH_URL = "https://gandi.v5/authorize" 103 | OAUTH2_TOKEN_URL = "https://gandi.v5/token" 104 | OAUTH2_LOGOUT_URL = "https://gandi.v5/logout" 105 | 106 | OAUTH2_VERIFY_CERTIFICATE = True 107 | OAUTH2_TIMEOUT = 30 108 | 109 | 110 | Consideration before activating OAUTH2_STORE_TOKENS 111 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | In OAuth2.0, tokens are for the app, not the user, so the session 114 | must be secure to avoid security issues. 115 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # This file is used only in tests 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wagtail_oauth2.tests.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "anyascii" 11 | version = "0.3.1" 12 | description = "Unicode to ASCII transliteration" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.3" 16 | 17 | [[package]] 18 | name = "asgiref" 19 | version = "3.5.2" 20 | description = "ASGI specs, helper code, and adapters" 21 | category = "main" 22 | optional = false 23 | python-versions = ">=3.7" 24 | 25 | [package.dependencies] 26 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 27 | 28 | [package.extras] 29 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 30 | 31 | [[package]] 32 | name = "atomicwrites" 33 | version = "1.4.0" 34 | description = "Atomic file writes." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 38 | 39 | [[package]] 40 | name = "attrs" 41 | version = "21.4.0" 42 | description = "Classes Without Boilerplate" 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 46 | 47 | [package.extras] 48 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 49 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 50 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 51 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 52 | 53 | [[package]] 54 | name = "babel" 55 | version = "2.10.2" 56 | description = "Internationalization utilities" 57 | category = "dev" 58 | optional = false 59 | python-versions = ">=3.6" 60 | 61 | [package.dependencies] 62 | pytz = ">=2015.7" 63 | 64 | [[package]] 65 | name = "beautifulsoup4" 66 | version = "4.9.3" 67 | description = "Screen-scraping library" 68 | category = "main" 69 | optional = false 70 | python-versions = "*" 71 | 72 | [package.dependencies] 73 | soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} 74 | 75 | [package.extras] 76 | html5lib = ["html5lib"] 77 | lxml = ["lxml"] 78 | 79 | [[package]] 80 | name = "black" 81 | version = "21.12b0" 82 | description = "The uncompromising code formatter." 83 | category = "dev" 84 | optional = false 85 | python-versions = ">=3.6.2" 86 | 87 | [package.dependencies] 88 | click = ">=7.1.2" 89 | mypy-extensions = ">=0.4.3" 90 | pathspec = ">=0.9.0,<1" 91 | platformdirs = ">=2" 92 | tomli = ">=0.2.6,<2.0.0" 93 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 94 | typing-extensions = [ 95 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 96 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 97 | ] 98 | 99 | [package.extras] 100 | colorama = ["colorama (>=0.4.3)"] 101 | d = ["aiohttp (>=3.7.4)"] 102 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 103 | python2 = ["typed-ast (>=1.4.3)"] 104 | uvloop = ["uvloop (>=0.15.2)"] 105 | 106 | [[package]] 107 | name = "certifi" 108 | version = "2022.5.18.1" 109 | description = "Python package for providing Mozilla's CA Bundle." 110 | category = "main" 111 | optional = false 112 | python-versions = ">=3.6" 113 | 114 | [[package]] 115 | name = "charset-normalizer" 116 | version = "2.0.12" 117 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 118 | category = "main" 119 | optional = false 120 | python-versions = ">=3.5.0" 121 | 122 | [package.extras] 123 | unicode_backport = ["unicodedata2"] 124 | 125 | [[package]] 126 | name = "click" 127 | version = "8.1.3" 128 | description = "Composable command line interface toolkit" 129 | category = "dev" 130 | optional = false 131 | python-versions = ">=3.7" 132 | 133 | [package.dependencies] 134 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 135 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 136 | 137 | [[package]] 138 | name = "colorama" 139 | version = "0.4.4" 140 | description = "Cross-platform colored terminal text." 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 144 | 145 | [[package]] 146 | name = "coverage" 147 | version = "6.4.1" 148 | description = "Code coverage measurement for Python" 149 | category = "dev" 150 | optional = false 151 | python-versions = ">=3.7" 152 | 153 | [package.dependencies] 154 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 155 | 156 | [package.extras] 157 | toml = ["tomli"] 158 | 159 | [[package]] 160 | name = "django" 161 | version = "3.2.13" 162 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 163 | category = "main" 164 | optional = false 165 | python-versions = ">=3.6" 166 | 167 | [package.dependencies] 168 | asgiref = ">=3.3.2,<4" 169 | pytz = "*" 170 | sqlparse = ">=0.2.2" 171 | 172 | [package.extras] 173 | argon2 = ["argon2-cffi (>=19.1.0)"] 174 | bcrypt = ["bcrypt"] 175 | 176 | [[package]] 177 | name = "django-filter" 178 | version = "21.1" 179 | description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." 180 | category = "main" 181 | optional = false 182 | python-versions = ">=3.6" 183 | 184 | [package.dependencies] 185 | Django = ">=2.2" 186 | 187 | [[package]] 188 | name = "django-modelcluster" 189 | version = "6.0" 190 | description = "Django extension to allow working with 'clusters' of models as a single unit, independently of the database" 191 | category = "main" 192 | optional = false 193 | python-versions = ">=3.7" 194 | 195 | [package.dependencies] 196 | django = ">=2.2" 197 | pytz = ">=2015.2" 198 | 199 | [package.extras] 200 | taggit = ["django-taggit (>=0.20)"] 201 | 202 | [[package]] 203 | name = "django-permissionedforms" 204 | version = "0.1" 205 | description = "Django extension for creating forms that vary according to user permissions" 206 | category = "main" 207 | optional = false 208 | python-versions = ">=3.7" 209 | 210 | [package.dependencies] 211 | Django = "*" 212 | 213 | [package.extras] 214 | testing = ["django-modelcluster"] 215 | 216 | [[package]] 217 | name = "django-taggit" 218 | version = "2.1.0" 219 | description = "django-taggit is a reusable Django application for simple tagging." 220 | category = "main" 221 | optional = false 222 | python-versions = ">=3.6" 223 | 224 | [package.dependencies] 225 | Django = ">=2.2" 226 | 227 | [[package]] 228 | name = "django-treebeard" 229 | version = "4.5.1" 230 | description = "Efficient tree implementations for Django" 231 | category = "main" 232 | optional = false 233 | python-versions = ">=3.6" 234 | 235 | [package.dependencies] 236 | Django = ">=2.2" 237 | 238 | [[package]] 239 | name = "djangorestframework" 240 | version = "3.13.1" 241 | description = "Web APIs for Django, made easy." 242 | category = "main" 243 | optional = false 244 | python-versions = ">=3.6" 245 | 246 | [package.dependencies] 247 | django = ">=2.2" 248 | pytz = "*" 249 | 250 | [[package]] 251 | name = "docutils" 252 | version = "0.17.1" 253 | description = "Docutils -- Python Documentation Utilities" 254 | category = "dev" 255 | optional = false 256 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 257 | 258 | [[package]] 259 | name = "draftjs-exporter" 260 | version = "2.1.7" 261 | description = "Library to convert rich text from Draft.js raw ContentState to HTML" 262 | category = "main" 263 | optional = false 264 | python-versions = "*" 265 | 266 | [package.extras] 267 | html5lib = ["beautifulsoup4 (>=4.4.1,<5)", "html5lib (>=0.999,<=1.0.1)"] 268 | lxml = ["lxml (>=4.2.0,<5)"] 269 | testing = ["tox (>=2.3.1)", "markov-draftjs (==0.1.1)", "memory-profiler (==0.47)", "psutil (==5.4.1)", "coverage (>=4.1.0)", "flake8 (>=3.2.0)", "isort (==4.2.5)", "beautifulsoup4 (>=4.4.1,<5)", "html5lib (>=0.999,<=1.0.1)", "lxml (>=4.2.0,<5)"] 270 | 271 | [[package]] 272 | name = "et-xmlfile" 273 | version = "1.1.0" 274 | description = "An implementation of lxml.xmlfile for the standard library" 275 | category = "main" 276 | optional = false 277 | python-versions = ">=3.6" 278 | 279 | [[package]] 280 | name = "faker" 281 | version = "10.0.0" 282 | description = "Faker is a Python package that generates fake data for you." 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=3.6" 286 | 287 | [package.dependencies] 288 | python-dateutil = ">=2.4" 289 | text-unidecode = "1.3" 290 | typing-extensions = {version = ">=3.10.0.2", markers = "python_version < \"3.8\""} 291 | 292 | [[package]] 293 | name = "html5lib" 294 | version = "1.1" 295 | description = "HTML parser based on the WHATWG HTML specification" 296 | category = "main" 297 | optional = false 298 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 299 | 300 | [package.dependencies] 301 | six = ">=1.9" 302 | webencodings = "*" 303 | 304 | [package.extras] 305 | all = ["genshi", "chardet (>=2.2)", "lxml"] 306 | chardet = ["chardet (>=2.2)"] 307 | genshi = ["genshi"] 308 | lxml = ["lxml"] 309 | 310 | [[package]] 311 | name = "idna" 312 | version = "3.3" 313 | description = "Internationalized Domain Names in Applications (IDNA)" 314 | category = "main" 315 | optional = false 316 | python-versions = ">=3.5" 317 | 318 | [[package]] 319 | name = "imagesize" 320 | version = "1.3.0" 321 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 322 | category = "dev" 323 | optional = false 324 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 325 | 326 | [[package]] 327 | name = "importlib-metadata" 328 | version = "4.11.4" 329 | description = "Read metadata from Python packages" 330 | category = "dev" 331 | optional = false 332 | python-versions = ">=3.7" 333 | 334 | [package.dependencies] 335 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 336 | zipp = ">=0.5" 337 | 338 | [package.extras] 339 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 340 | perf = ["ipython"] 341 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] 342 | 343 | [[package]] 344 | name = "iniconfig" 345 | version = "1.1.1" 346 | description = "iniconfig: brain-dead simple config-ini parsing" 347 | category = "dev" 348 | optional = false 349 | python-versions = "*" 350 | 351 | [[package]] 352 | name = "isort" 353 | version = "5.10.1" 354 | description = "A Python utility / library to sort Python imports." 355 | category = "dev" 356 | optional = false 357 | python-versions = ">=3.6.1,<4.0" 358 | 359 | [package.extras] 360 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 361 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 362 | colors = ["colorama (>=0.4.3,<0.5.0)"] 363 | plugins = ["setuptools"] 364 | 365 | [[package]] 366 | name = "jinja2" 367 | version = "3.1.2" 368 | description = "A very fast and expressive template engine." 369 | category = "dev" 370 | optional = false 371 | python-versions = ">=3.7" 372 | 373 | [package.dependencies] 374 | MarkupSafe = ">=2.0" 375 | 376 | [package.extras] 377 | i18n = ["Babel (>=2.7)"] 378 | 379 | [[package]] 380 | name = "l18n" 381 | version = "2021.3" 382 | description = "Internationalization for pytz timezones and territories" 383 | category = "main" 384 | optional = false 385 | python-versions = "*" 386 | 387 | [package.dependencies] 388 | pytz = ">=2020.1" 389 | six = "*" 390 | 391 | [[package]] 392 | name = "markupsafe" 393 | version = "2.1.1" 394 | description = "Safely add untrusted strings to HTML/XML markup." 395 | category = "dev" 396 | optional = false 397 | python-versions = ">=3.7" 398 | 399 | [[package]] 400 | name = "mypy-extensions" 401 | version = "0.4.3" 402 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 403 | category = "dev" 404 | optional = false 405 | python-versions = "*" 406 | 407 | [[package]] 408 | name = "openpyxl" 409 | version = "3.0.10" 410 | description = "A Python library to read/write Excel 2010 xlsx/xlsm files" 411 | category = "main" 412 | optional = false 413 | python-versions = ">=3.6" 414 | 415 | [package.dependencies] 416 | et-xmlfile = "*" 417 | 418 | [[package]] 419 | name = "packaging" 420 | version = "21.3" 421 | description = "Core utilities for Python packages" 422 | category = "dev" 423 | optional = false 424 | python-versions = ">=3.6" 425 | 426 | [package.dependencies] 427 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 428 | 429 | [[package]] 430 | name = "pathspec" 431 | version = "0.9.0" 432 | description = "Utility library for gitignore style pattern matching of file paths." 433 | category = "dev" 434 | optional = false 435 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 436 | 437 | [[package]] 438 | name = "pillow" 439 | version = "9.1.1" 440 | description = "Python Imaging Library (Fork)" 441 | category = "main" 442 | optional = false 443 | python-versions = ">=3.7" 444 | 445 | [package.extras] 446 | docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] 447 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 448 | 449 | [[package]] 450 | name = "platformdirs" 451 | version = "2.5.2" 452 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 453 | category = "dev" 454 | optional = false 455 | python-versions = ">=3.7" 456 | 457 | [package.extras] 458 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 459 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 460 | 461 | [[package]] 462 | name = "pluggy" 463 | version = "1.0.0" 464 | description = "plugin and hook calling mechanisms for python" 465 | category = "dev" 466 | optional = false 467 | python-versions = ">=3.6" 468 | 469 | [package.dependencies] 470 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 471 | 472 | [package.extras] 473 | dev = ["pre-commit", "tox"] 474 | testing = ["pytest", "pytest-benchmark"] 475 | 476 | [[package]] 477 | name = "py" 478 | version = "1.11.0" 479 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 480 | category = "dev" 481 | optional = false 482 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 483 | 484 | [[package]] 485 | name = "pygments" 486 | version = "2.12.0" 487 | description = "Pygments is a syntax highlighting package written in Python." 488 | category = "dev" 489 | optional = false 490 | python-versions = ">=3.6" 491 | 492 | [[package]] 493 | name = "pyparsing" 494 | version = "3.0.9" 495 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 496 | category = "dev" 497 | optional = false 498 | python-versions = ">=3.6.8" 499 | 500 | [package.extras] 501 | diagrams = ["railroad-diagrams", "jinja2"] 502 | 503 | [[package]] 504 | name = "pytest" 505 | version = "7.1.2" 506 | description = "pytest: simple powerful testing with Python" 507 | category = "dev" 508 | optional = false 509 | python-versions = ">=3.7" 510 | 511 | [package.dependencies] 512 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 513 | attrs = ">=19.2.0" 514 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 515 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 516 | iniconfig = "*" 517 | packaging = "*" 518 | pluggy = ">=0.12,<2.0" 519 | py = ">=1.8.2" 520 | tomli = ">=1.0.0" 521 | 522 | [package.extras] 523 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 524 | 525 | [[package]] 526 | name = "pytest-cov" 527 | version = "3.0.0" 528 | description = "Pytest plugin for measuring coverage." 529 | category = "dev" 530 | optional = false 531 | python-versions = ">=3.6" 532 | 533 | [package.dependencies] 534 | coverage = {version = ">=5.2.1", extras = ["toml"]} 535 | pytest = ">=4.6" 536 | 537 | [package.extras] 538 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 539 | 540 | [[package]] 541 | name = "pytest-django" 542 | version = "4.5.2" 543 | description = "A Django plugin for pytest." 544 | category = "dev" 545 | optional = false 546 | python-versions = ">=3.5" 547 | 548 | [package.dependencies] 549 | pytest = ">=5.4.0" 550 | 551 | [package.extras] 552 | docs = ["sphinx", "sphinx-rtd-theme"] 553 | testing = ["django", "django-configurations (>=2.0)"] 554 | 555 | [[package]] 556 | name = "python-dateutil" 557 | version = "2.8.2" 558 | description = "Extensions to the standard Python datetime module" 559 | category = "dev" 560 | optional = false 561 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 562 | 563 | [package.dependencies] 564 | six = ">=1.5" 565 | 566 | [[package]] 567 | name = "pytz" 568 | version = "2022.1" 569 | description = "World timezone definitions, modern and historical" 570 | category = "main" 571 | optional = false 572 | python-versions = "*" 573 | 574 | [[package]] 575 | name = "requests" 576 | version = "2.28.0" 577 | description = "Python HTTP for Humans." 578 | category = "main" 579 | optional = false 580 | python-versions = ">=3.7, <4" 581 | 582 | [package.dependencies] 583 | certifi = ">=2017.4.17" 584 | charset-normalizer = ">=2.0.0,<2.1.0" 585 | idna = ">=2.5,<4" 586 | urllib3 = ">=1.21.1,<1.27" 587 | 588 | [package.extras] 589 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 590 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 591 | 592 | [[package]] 593 | name = "six" 594 | version = "1.16.0" 595 | description = "Python 2 and 3 compatibility utilities" 596 | category = "main" 597 | optional = false 598 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 599 | 600 | [[package]] 601 | name = "snowballstemmer" 602 | version = "2.2.0" 603 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 604 | category = "dev" 605 | optional = false 606 | python-versions = "*" 607 | 608 | [[package]] 609 | name = "soupsieve" 610 | version = "2.3.2.post1" 611 | description = "A modern CSS selector implementation for Beautiful Soup." 612 | category = "main" 613 | optional = false 614 | python-versions = ">=3.6" 615 | 616 | [[package]] 617 | name = "sphinx" 618 | version = "4.5.0" 619 | description = "Python documentation generator" 620 | category = "dev" 621 | optional = false 622 | python-versions = ">=3.6" 623 | 624 | [package.dependencies] 625 | alabaster = ">=0.7,<0.8" 626 | babel = ">=1.3" 627 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} 628 | docutils = ">=0.14,<0.18" 629 | imagesize = "*" 630 | importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} 631 | Jinja2 = ">=2.3" 632 | packaging = "*" 633 | Pygments = ">=2.0" 634 | requests = ">=2.5.0" 635 | snowballstemmer = ">=1.1" 636 | sphinxcontrib-applehelp = "*" 637 | sphinxcontrib-devhelp = "*" 638 | sphinxcontrib-htmlhelp = ">=2.0.0" 639 | sphinxcontrib-jsmath = "*" 640 | sphinxcontrib-qthelp = "*" 641 | sphinxcontrib-serializinghtml = ">=1.1.5" 642 | 643 | [package.extras] 644 | docs = ["sphinxcontrib-websupport"] 645 | lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] 646 | test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] 647 | 648 | [[package]] 649 | name = "sphinx-rtd-theme" 650 | version = "1.0.0" 651 | description = "Read the Docs theme for Sphinx" 652 | category = "dev" 653 | optional = false 654 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 655 | 656 | [package.dependencies] 657 | docutils = "<0.18" 658 | sphinx = ">=1.6" 659 | 660 | [package.extras] 661 | dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] 662 | 663 | [[package]] 664 | name = "sphinxcontrib-applehelp" 665 | version = "1.0.2" 666 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 667 | category = "dev" 668 | optional = false 669 | python-versions = ">=3.5" 670 | 671 | [package.extras] 672 | lint = ["flake8", "mypy", "docutils-stubs"] 673 | test = ["pytest"] 674 | 675 | [[package]] 676 | name = "sphinxcontrib-devhelp" 677 | version = "1.0.2" 678 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 679 | category = "dev" 680 | optional = false 681 | python-versions = ">=3.5" 682 | 683 | [package.extras] 684 | lint = ["flake8", "mypy", "docutils-stubs"] 685 | test = ["pytest"] 686 | 687 | [[package]] 688 | name = "sphinxcontrib-htmlhelp" 689 | version = "2.0.0" 690 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 691 | category = "dev" 692 | optional = false 693 | python-versions = ">=3.6" 694 | 695 | [package.extras] 696 | lint = ["flake8", "mypy", "docutils-stubs"] 697 | test = ["pytest", "html5lib"] 698 | 699 | [[package]] 700 | name = "sphinxcontrib-jsmath" 701 | version = "1.0.1" 702 | description = "A sphinx extension which renders display math in HTML via JavaScript" 703 | category = "dev" 704 | optional = false 705 | python-versions = ">=3.5" 706 | 707 | [package.extras] 708 | test = ["pytest", "flake8", "mypy"] 709 | 710 | [[package]] 711 | name = "sphinxcontrib-qthelp" 712 | version = "1.0.3" 713 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 714 | category = "dev" 715 | optional = false 716 | python-versions = ">=3.5" 717 | 718 | [package.extras] 719 | lint = ["flake8", "mypy", "docutils-stubs"] 720 | test = ["pytest"] 721 | 722 | [[package]] 723 | name = "sphinxcontrib-serializinghtml" 724 | version = "1.1.5" 725 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 726 | category = "dev" 727 | optional = false 728 | python-versions = ">=3.5" 729 | 730 | [package.extras] 731 | lint = ["flake8", "mypy", "docutils-stubs"] 732 | test = ["pytest"] 733 | 734 | [[package]] 735 | name = "sqlparse" 736 | version = "0.4.2" 737 | description = "A non-validating SQL parser." 738 | category = "main" 739 | optional = false 740 | python-versions = ">=3.5" 741 | 742 | [[package]] 743 | name = "tablib" 744 | version = "3.2.1" 745 | description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV)" 746 | category = "main" 747 | optional = false 748 | python-versions = ">=3.7" 749 | 750 | [package.dependencies] 751 | openpyxl = {version = ">=2.6.0", optional = true, markers = "extra == \"xlsx\""} 752 | xlrd = {version = "*", optional = true, markers = "extra == \"xls\""} 753 | xlwt = {version = "*", optional = true, markers = "extra == \"xls\""} 754 | 755 | [package.extras] 756 | all = ["markuppy", "odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"] 757 | cli = ["tabulate"] 758 | html = ["markuppy"] 759 | ods = ["odfpy"] 760 | pandas = ["pandas"] 761 | xls = ["xlrd", "xlwt"] 762 | xlsx = ["openpyxl (>=2.6.0)"] 763 | yaml = ["pyyaml"] 764 | 765 | [[package]] 766 | name = "telepath" 767 | version = "0.2" 768 | description = "A library for exchanging data between Python and JavaScript" 769 | category = "main" 770 | optional = false 771 | python-versions = ">=3.5" 772 | 773 | [package.extras] 774 | docs = ["mkdocs (>=1.1,<1.2)", "mkdocs-material (>=6.2,<6.3)"] 775 | 776 | [[package]] 777 | name = "text-unidecode" 778 | version = "1.3" 779 | description = "The most basic Text::Unidecode port" 780 | category = "dev" 781 | optional = false 782 | python-versions = "*" 783 | 784 | [[package]] 785 | name = "tomli" 786 | version = "1.2.3" 787 | description = "A lil' TOML parser" 788 | category = "dev" 789 | optional = false 790 | python-versions = ">=3.6" 791 | 792 | [[package]] 793 | name = "tomlkit" 794 | version = "0.7.2" 795 | description = "Style preserving TOML library" 796 | category = "dev" 797 | optional = false 798 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 799 | 800 | [[package]] 801 | name = "typed-ast" 802 | version = "1.5.4" 803 | description = "a fork of Python 2 and 3 ast modules with type comment support" 804 | category = "dev" 805 | optional = false 806 | python-versions = ">=3.6" 807 | 808 | [[package]] 809 | name = "typing-extensions" 810 | version = "4.2.0" 811 | description = "Backported and Experimental Type Hints for Python 3.7+" 812 | category = "main" 813 | optional = false 814 | python-versions = ">=3.7" 815 | 816 | [[package]] 817 | name = "urllib3" 818 | version = "1.26.9" 819 | description = "HTTP library with thread-safe connection pooling, file post, and more." 820 | category = "main" 821 | optional = false 822 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 823 | 824 | [package.extras] 825 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 826 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 827 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 828 | 829 | [[package]] 830 | name = "wagtail" 831 | version = "3.0" 832 | description = "A Django content management system." 833 | category = "main" 834 | optional = false 835 | python-versions = ">=3.7" 836 | 837 | [package.dependencies] 838 | anyascii = ">=0.1.5" 839 | beautifulsoup4 = ">=4.8,<4.10" 840 | Django = ">=3.2,<4.1" 841 | django-filter = ">=2.2,<22" 842 | django-modelcluster = ">=6.0,<7.0" 843 | django-permissionedforms = ">=0.1,<1.0" 844 | django-taggit = ">=2.0,<3.0" 845 | django-treebeard = ">=4.5.1,<5.0" 846 | djangorestframework = ">=3.11.1,<4.0" 847 | draftjs-exporter = ">=2.1.5,<3.0" 848 | html5lib = ">=0.999,<2" 849 | l18n = ">=2018.5" 850 | Pillow = ">=4.0.0,<10.0.0" 851 | requests = ">=2.11.1,<3.0" 852 | tablib = {version = ">=0.14.0", extras = ["xls", "xlsx"]} 853 | telepath = ">=0.1.1,<1" 854 | Willow = ">=1.4,<1.5" 855 | xlsxwriter = ">=1.2.8,<4.0" 856 | 857 | [package.extras] 858 | docs = ["pyenchant (>=3.1.1,<4)", "sphinxcontrib-spelling (>=5.4.0,<6)", "Sphinx (>=1.5.2)", "sphinx-autobuild (>=0.6.0)", "sphinx-wagtail-theme (==5.1.1)", "myst-parser (==0.17.0)"] 859 | testing = ["python-dateutil (>=2.7)", "pytz (>=2014.7)", "elasticsearch (>=5.0,<6.0)", "Jinja2 (>=3.0,<3.2)", "boto3 (>=1.16,<1.17)", "freezegun (>=0.3.8)", "openpyxl (>=2.6.4)", "azure-mgmt-cdn (>=5.1,<6.0)", "azure-mgmt-frontdoor (>=0.3,<0.4)", "django-pattern-library (>=0.7,<0.8)", "coverage (>=3.7.0)", "black (==22.3.0)", "flake8 (>=3.6.0)", "isort (==5.6.4)", "flake8-blind-except (==0.1.1)", "flake8-comprehensions (==3.8.0)", "flake8-print (==2.0.2)", "doc8 (==0.8.1)", "flake8-assertive (==2.0.0)", "curlylint (==0.13.1)", "djhtml (==1.4.13)", "polib (>=1.1,<2.0)"] 860 | 861 | [[package]] 862 | name = "webencodings" 863 | version = "0.5.1" 864 | description = "Character encoding aliases for legacy web content" 865 | category = "main" 866 | optional = false 867 | python-versions = "*" 868 | 869 | [[package]] 870 | name = "willow" 871 | version = "1.4.1" 872 | description = "A Python image library that sits on top of Pillow, Wand and OpenCV" 873 | category = "main" 874 | optional = false 875 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 876 | 877 | [package.extras] 878 | testing = ["Pillow (>=6.0.0,<10.0.0)", "Wand (>=0.6,<1.0)", "mock (>=3.0,<4.0)"] 879 | 880 | [[package]] 881 | name = "xlrd" 882 | version = "2.0.1" 883 | description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" 884 | category = "main" 885 | optional = false 886 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 887 | 888 | [package.extras] 889 | build = ["wheel", "twine"] 890 | docs = ["sphinx"] 891 | test = ["pytest", "pytest-cov"] 892 | 893 | [[package]] 894 | name = "xlsxwriter" 895 | version = "3.0.3" 896 | description = "A Python module for creating Excel XLSX files." 897 | category = "main" 898 | optional = false 899 | python-versions = ">=3.4" 900 | 901 | [[package]] 902 | name = "xlwt" 903 | version = "1.3.0" 904 | description = "Library to create spreadsheet files compatible with MS Excel 97/2000/XP/2003 XLS files, on any platform, with Python 2.6, 2.7, 3.3+" 905 | category = "main" 906 | optional = false 907 | python-versions = "*" 908 | 909 | [[package]] 910 | name = "xmlrunner" 911 | version = "1.7.7" 912 | description = "PyUnit-based test runner with JUnit like XML reporting." 913 | category = "dev" 914 | optional = false 915 | python-versions = "*" 916 | 917 | [[package]] 918 | name = "zipp" 919 | version = "3.8.0" 920 | description = "Backport of pathlib-compatible object wrapper for zip files" 921 | category = "dev" 922 | optional = false 923 | python-versions = ">=3.7" 924 | 925 | [package.extras] 926 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 927 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] 928 | 929 | [metadata] 930 | lock-version = "1.1" 931 | python-versions = "^3.7" 932 | content-hash = "ac79e9775ada8247f0db382a25eb7d23fb0ef721c41f1bb065f757f7ea6786f0" 933 | 934 | [metadata.files] 935 | alabaster = [ 936 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 937 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 938 | ] 939 | anyascii = [ 940 | {file = "anyascii-0.3.1-py3-none-any.whl", hash = "sha256:8707d3185017435933360462a65e2c70a4818490745804f38a5ca55e59eb56a0"}, 941 | {file = "anyascii-0.3.1.tar.gz", hash = "sha256:dedf57728206e286c91eed7c759505a5e45c8cd01367dd40c2f7248bb15c11f6"}, 942 | ] 943 | asgiref = [ 944 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 945 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 946 | ] 947 | atomicwrites = [ 948 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 949 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 950 | ] 951 | attrs = [ 952 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 953 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 954 | ] 955 | babel = [ 956 | {file = "Babel-2.10.2-py3-none-any.whl", hash = "sha256:81a3beca4d0cd40a9cfb9e2adb2cf39261c2f959b92e7a74750befe5d79afd7b"}, 957 | {file = "Babel-2.10.2.tar.gz", hash = "sha256:7aed055f0c04c9e7f51a2f75261e41e1c804efa724cb65b60a970dd4448d469d"}, 958 | ] 959 | beautifulsoup4 = [ 960 | {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, 961 | {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, 962 | {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, 963 | ] 964 | black = [ 965 | {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, 966 | {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, 967 | ] 968 | certifi = [ 969 | {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, 970 | {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, 971 | ] 972 | charset-normalizer = [ 973 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 974 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 975 | ] 976 | click = [ 977 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 978 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 979 | ] 980 | colorama = [ 981 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 982 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 983 | ] 984 | coverage = [ 985 | {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, 986 | {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, 987 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, 988 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, 989 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, 990 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, 991 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, 992 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, 993 | {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, 994 | {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, 995 | {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, 996 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, 997 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, 998 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, 999 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, 1000 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, 1001 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, 1002 | {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, 1003 | {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, 1004 | {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, 1005 | {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, 1006 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, 1007 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, 1008 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, 1009 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, 1010 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, 1011 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, 1012 | {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, 1013 | {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, 1014 | {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, 1015 | {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, 1016 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, 1017 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, 1018 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, 1019 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, 1020 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, 1021 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, 1022 | {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, 1023 | {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, 1024 | {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, 1025 | {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, 1026 | ] 1027 | django = [ 1028 | {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, 1029 | {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, 1030 | ] 1031 | django-filter = [ 1032 | {file = "django-filter-21.1.tar.gz", hash = "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e"}, 1033 | {file = "django_filter-21.1-py3-none-any.whl", hash = "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"}, 1034 | ] 1035 | django-modelcluster = [ 1036 | {file = "django-modelcluster-6.0.tar.gz", hash = "sha256:cdcffef5baf5d3759ee04c5b60ffaf1a0c95fc0f265e762f3ddfadde3394e5db"}, 1037 | {file = "django_modelcluster-6.0-py2.py3-none-any.whl", hash = "sha256:4ae46f86c43702020f24f212222eef0a2588df937bbb523a5447da247b5fb130"}, 1038 | ] 1039 | django-permissionedforms = [ 1040 | {file = "django-permissionedforms-0.1.tar.gz", hash = "sha256:4340bb20c4477fffb13b4cc5cccf9f1b1010b64f79956c291c72d2ad2ed243f8"}, 1041 | {file = "django_permissionedforms-0.1-py2.py3-none-any.whl", hash = "sha256:d341a961a27cc77fde8cc42141c6ab55cc1f0cb886963cc2d6967b9674fa47d6"}, 1042 | ] 1043 | django-taggit = [ 1044 | {file = "django-taggit-2.1.0.tar.gz", hash = "sha256:a9f41e4ad58efe4b28d86f274728ee87eb98eeae90c9eb4b4efad39e5068184e"}, 1045 | {file = "django_taggit-2.1.0-py3-none-any.whl", hash = "sha256:61547a23fc99967c9304107414a09e662b459f4163dbbae32e60b8ba40c34d05"}, 1046 | ] 1047 | django-treebeard = [ 1048 | {file = "django-treebeard-4.5.1.tar.gz", hash = "sha256:80150017725239702054e5fa64dc66e383dc13ac262c8d47ee5a82cb005969da"}, 1049 | {file = "django_treebeard-4.5.1-py3-none-any.whl", hash = "sha256:7c2b1cdb1e9b46d595825186064a1228bc4d00dbbc186db5b0b9412357fba91c"}, 1050 | ] 1051 | djangorestframework = [ 1052 | {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, 1053 | {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, 1054 | ] 1055 | docutils = [ 1056 | {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, 1057 | {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, 1058 | ] 1059 | draftjs-exporter = [ 1060 | {file = "draftjs_exporter-2.1.7-py3-none-any.whl", hash = "sha256:d415a9964690a2cddb66a31ef32dd46c277e9b80434b94e39e3043188ed83e33"}, 1061 | {file = "draftjs_exporter-2.1.7.tar.gz", hash = "sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb"}, 1062 | ] 1063 | et-xmlfile = [ 1064 | {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, 1065 | {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, 1066 | ] 1067 | faker = [ 1068 | {file = "Faker-10.0.0-py3-none-any.whl", hash = "sha256:3163c84866cf118ac5329a802e046b0f729528ce62ebb2806b626e0badbb6ff3"}, 1069 | {file = "Faker-10.0.0.tar.gz", hash = "sha256:530690ad12a2a054071af95fc8a354c5fd57b5e7707053a9662f40f14a87b68e"}, 1070 | ] 1071 | html5lib = [ 1072 | {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, 1073 | {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, 1074 | ] 1075 | idna = [ 1076 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 1077 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 1078 | ] 1079 | imagesize = [ 1080 | {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, 1081 | {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, 1082 | ] 1083 | importlib-metadata = [ 1084 | {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, 1085 | {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, 1086 | ] 1087 | iniconfig = [ 1088 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 1089 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 1090 | ] 1091 | isort = [ 1092 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 1093 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 1094 | ] 1095 | jinja2 = [ 1096 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 1097 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 1098 | ] 1099 | l18n = [ 1100 | {file = "l18n-2021.3-py3-none-any.whl", hash = "sha256:78495d1df95b6f7dcc694d1ba8994df709c463a1cbac1bf016e1b9a5ce7280b9"}, 1101 | {file = "l18n-2021.3.tar.gz", hash = "sha256:1956e890d673d17135cc20913253c154f6bc1c00266c22b7d503cc1a5a42d848"}, 1102 | ] 1103 | markupsafe = [ 1104 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 1105 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 1106 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 1107 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 1108 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 1109 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 1110 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 1111 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 1112 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 1113 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 1114 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 1115 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 1116 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 1117 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 1118 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 1119 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 1120 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 1121 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 1122 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 1123 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 1124 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 1125 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 1126 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 1127 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 1128 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 1129 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 1130 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 1131 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 1132 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 1133 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 1134 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 1135 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 1136 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 1137 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 1138 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 1139 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 1140 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 1141 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 1142 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 1143 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 1144 | ] 1145 | mypy-extensions = [ 1146 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1147 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1148 | ] 1149 | openpyxl = [ 1150 | {file = "openpyxl-3.0.10-py2.py3-none-any.whl", hash = "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355"}, 1151 | {file = "openpyxl-3.0.10.tar.gz", hash = "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449"}, 1152 | ] 1153 | packaging = [ 1154 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 1155 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 1156 | ] 1157 | pathspec = [ 1158 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 1159 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 1160 | ] 1161 | pillow = [ 1162 | {file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"}, 1163 | {file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"}, 1164 | {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"}, 1165 | {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"}, 1166 | {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"}, 1167 | {file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"}, 1168 | {file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"}, 1169 | {file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"}, 1170 | {file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"}, 1171 | {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"}, 1172 | {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"}, 1173 | {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"}, 1174 | {file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"}, 1175 | {file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"}, 1176 | {file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"}, 1177 | {file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"}, 1178 | {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"}, 1179 | {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"}, 1180 | {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"}, 1181 | {file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"}, 1182 | {file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"}, 1183 | {file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"}, 1184 | {file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"}, 1185 | {file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"}, 1186 | {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"}, 1187 | {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"}, 1188 | {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"}, 1189 | {file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"}, 1190 | {file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"}, 1191 | {file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"}, 1192 | {file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"}, 1193 | {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"}, 1194 | {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"}, 1195 | {file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"}, 1196 | {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"}, 1197 | {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"}, 1198 | {file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"}, 1199 | {file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"}, 1200 | ] 1201 | platformdirs = [ 1202 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 1203 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 1204 | ] 1205 | pluggy = [ 1206 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1207 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1208 | ] 1209 | py = [ 1210 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 1211 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 1212 | ] 1213 | pygments = [ 1214 | {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, 1215 | {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, 1216 | ] 1217 | pyparsing = [ 1218 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 1219 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 1220 | ] 1221 | pytest = [ 1222 | {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, 1223 | {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, 1224 | ] 1225 | pytest-cov = [ 1226 | {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, 1227 | {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, 1228 | ] 1229 | pytest-django = [ 1230 | {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, 1231 | {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, 1232 | ] 1233 | python-dateutil = [ 1234 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1235 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1236 | ] 1237 | pytz = [ 1238 | {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, 1239 | {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, 1240 | ] 1241 | requests = [ 1242 | {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, 1243 | {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, 1244 | ] 1245 | six = [ 1246 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1247 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1248 | ] 1249 | snowballstemmer = [ 1250 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 1251 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 1252 | ] 1253 | soupsieve = [ 1254 | {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, 1255 | {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, 1256 | ] 1257 | sphinx = [ 1258 | {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, 1259 | {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, 1260 | ] 1261 | sphinx-rtd-theme = [ 1262 | {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, 1263 | {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, 1264 | ] 1265 | sphinxcontrib-applehelp = [ 1266 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, 1267 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, 1268 | ] 1269 | sphinxcontrib-devhelp = [ 1270 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 1271 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 1272 | ] 1273 | sphinxcontrib-htmlhelp = [ 1274 | {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, 1275 | {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, 1276 | ] 1277 | sphinxcontrib-jsmath = [ 1278 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 1279 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 1280 | ] 1281 | sphinxcontrib-qthelp = [ 1282 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 1283 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 1284 | ] 1285 | sphinxcontrib-serializinghtml = [ 1286 | {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, 1287 | {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, 1288 | ] 1289 | sqlparse = [ 1290 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 1291 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 1292 | ] 1293 | tablib = [ 1294 | {file = "tablib-3.2.1-py3-none-any.whl", hash = "sha256:870d7e688f738531a14937a055e8bba404fbc388e77d4d500b2c904075d1019c"}, 1295 | {file = "tablib-3.2.1.tar.gz", hash = "sha256:a57f2770b8c225febec1cb1e65012a69cf30dd28be810e0ff98d024768c7d0f1"}, 1296 | ] 1297 | telepath = [ 1298 | {file = "telepath-0.2-py35-none-any.whl", hash = "sha256:801615094d3d964e178183099bf04020f4ff9c84ec43945d40b096df0a5767ee"}, 1299 | {file = "telepath-0.2.tar.gz", hash = "sha256:ef4cf2a45ed1908c58639c346756955f8a73ae79002a8116d596b3fd702bf84c"}, 1300 | ] 1301 | text-unidecode = [ 1302 | {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, 1303 | {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, 1304 | ] 1305 | tomli = [ 1306 | {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, 1307 | {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, 1308 | ] 1309 | tomlkit = [ 1310 | {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, 1311 | {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, 1312 | ] 1313 | typed-ast = [ 1314 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 1315 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 1316 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 1317 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 1318 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 1319 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 1320 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 1321 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 1322 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 1323 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 1324 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 1325 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 1326 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 1327 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 1328 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 1329 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 1330 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 1331 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 1332 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 1333 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 1334 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 1335 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 1336 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 1337 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 1338 | ] 1339 | typing-extensions = [ 1340 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 1341 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 1342 | ] 1343 | urllib3 = [ 1344 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 1345 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 1346 | ] 1347 | wagtail = [ 1348 | {file = "wagtail-3.0-py3-none-any.whl", hash = "sha256:4a844d2d1cf5cbc9c48344c6ae9cc5c9f0d1134cd45cd2c484afdedc1164043a"}, 1349 | {file = "wagtail-3.0.tar.gz", hash = "sha256:d54d0742c5cebf92b021c04a6ac93312b8cb5f55e7c10f37199938033bda905d"}, 1350 | ] 1351 | webencodings = [ 1352 | {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, 1353 | {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, 1354 | ] 1355 | willow = [ 1356 | {file = "Willow-1.4.1-py2.py3-none-any.whl", hash = "sha256:fc4042696d090e75aef922fa1ed26d483c764f005b36cf523cf7c34e69d5dd7a"}, 1357 | {file = "Willow-1.4.1.tar.gz", hash = "sha256:0df8ff528531e00b48d40bf72ed81beac1dc82f2d42e5bbed4aff0218bef8c0d"}, 1358 | ] 1359 | xlrd = [ 1360 | {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"}, 1361 | {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"}, 1362 | ] 1363 | xlsxwriter = [ 1364 | {file = "XlsxWriter-3.0.3-py3-none-any.whl", hash = "sha256:df0aefe5137478d206847eccf9f114715e42aaea077e6a48d0e8a2152e983010"}, 1365 | {file = "XlsxWriter-3.0.3.tar.gz", hash = "sha256:e89f4a1d2fa2c9ea15cde77de95cd3fd8b0345d0efb3964623f395c8c4988b7f"}, 1366 | ] 1367 | xlwt = [ 1368 | {file = "xlwt-1.3.0-py2.py3-none-any.whl", hash = "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e"}, 1369 | {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"}, 1370 | ] 1371 | xmlrunner = [ 1372 | {file = "xmlrunner-1.7.7.tar.gz", hash = "sha256:5a6113d049eca7646111ee657266966e5bbfb0b5ceb2e83ee0772e16d7110f39"}, 1373 | ] 1374 | zipp = [ 1375 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 1376 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 1377 | ] 1378 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wagtail-oauth2" 3 | version = "1.0.0" 4 | description = "OAuth2.0 authentication fo wagtail" 5 | authors = ["Guillaume Gauvrit "] 6 | readme = "README.rst" 7 | license = "BSD-Derived" 8 | classifiers = [ 9 | "Intended Audience :: Developers", 10 | "Topic :: Software Development :: Libraries :: Python Modules", 11 | "Topic :: Internet :: WWW/HTTP", 12 | "Framework :: Wagtail", 13 | "Framework :: Wagtail :: 2", 14 | "License :: OSI Approved :: BSD License" 15 | ] 16 | homepage = "https://wagtail-oauth2.readthedocs.io/en/latest/" 17 | repository = "https://github.com/Gandi/wagtail-oauth2" 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.7" 21 | Django = ">=3.2.0,<5" 22 | wagtail = ">=2.14,<4.0" 23 | requests = ">=2.26.0" 24 | 25 | [tool.poetry.dev-dependencies] 26 | black = "^21.11b1" 27 | isort = "^5.10.1" 28 | pytest-django = "^4.4.0" 29 | xmlrunner = "^1.7.7" 30 | pytest-cov = "^3.0.0" 31 | Faker = "^10.0.0" 32 | Sphinx = "^4.3.1" 33 | sphinx-rtd-theme = "^1.0.0" 34 | tomlkit = "^0.7.2" 35 | 36 | [tool.isort] 37 | profile = "black" 38 | multi_line_output = 3 39 | line_length = 88 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.0.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = wagtail_oauth2.tests.settings 3 | python_files = test_*.py 4 | addopts = --reuse-db --junitxml=tests/test.xml --cov=wagtail_oauth2 --cov-report=term-missing --cov-report=html --cov-report=xml:tests/coverage.xml 5 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | try: 4 | __version__ = pkg_resources.get_distribution("wagtail_oauth2").version 5 | except pkg_resources.DistributionNotFound: 6 | # Read the docs does not support poetry and cannot install the package 7 | __version__ = None 8 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manage OAuth2.0 Resources. 3 | """ 4 | import logging 5 | 6 | from urllib.parse import urlencode 7 | 8 | import requests 9 | from requests.exceptions import HTTPError, ConnectionError, Timeout 10 | 11 | from .settings import get_setting 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class Token: 17 | """Retrieve tokens from the OAuth2.0 Resource server.""" 18 | @classmethod 19 | def by_authcode(cls, auth_code): 20 | "Retrieve Tokens from an authorization code." 21 | try: 22 | token_url = get_setting("TOKEN_URL") 23 | log.info("Fetching token on %s/token" % token_url) 24 | response = requests.post( 25 | token_url, 26 | data={ 27 | "client_id": get_setting("CLIENT_ID"), 28 | "client_secret": get_setting("CLIENT_SECRET"), 29 | "grant_type": "authorization_code", 30 | "code": auth_code, 31 | }, 32 | verify=get_setting("VERIFY_CERTIFICATE", True), 33 | timeout=get_setting("TIMEOUT", 30), 34 | ) 35 | response.raise_for_status() 36 | return response.json() 37 | 38 | except (ConnectionError, TimeoutError, Timeout) as exc: 39 | log.error("OAuth2 server is down: %s" % exc.__class__.__name__) 40 | except HTTPError as exc: 41 | if exc.response.status_code >= 500: 42 | log.error( 43 | "OAuth2 server is not working properly? got %s" 44 | % exc.response.status_code 45 | ) 46 | else: 47 | log.error("Cannot retrieve token: %s" % exc.response.text) 48 | except Exception: 49 | log.exception( 50 | "Unexpected exception while retrieving token using " 51 | "authorization code" 52 | ) 53 | return {} 54 | 55 | @classmethod 56 | def get_authenticated_url(cls, login_url, state): 57 | "Get the authorization url with its parameter." 58 | data = { 59 | "client_id": get_setting("CLIENT_ID"), 60 | "redirect_uri": login_url, 61 | "response_type": "code", 62 | "state": state, 63 | } 64 | return "{}?{}".format( 65 | get_setting("AUTH_URL"), urlencode(data, doseq=True) 66 | ) 67 | 68 | @classmethod 69 | def by_refresh_token(cls, refresh_token): 70 | "Retrieve Tokens from a refresh tokend." 71 | try: 72 | token_url = get_setting("TOKEN_URL") 73 | log.info("Refreshing token on %s/token" % token_url) 74 | response = requests.post( 75 | token_url, 76 | data={ 77 | "client_id": get_setting("CLIENT_ID"), 78 | "client_secret": get_setting("CLIENT_SECRET"), 79 | "grant_type": "refresh_token", 80 | "refresh_token": refresh_token, 81 | }, 82 | verify=get_setting("VERIFY_CERTIFICATE", True), 83 | timeout=get_setting("TIMEOUT", 30), 84 | ) 85 | response.raise_for_status() 86 | tokens = response.json() 87 | return tokens 88 | except (ConnectionError, TimeoutError, Timeout) as exc: 89 | log.error("OAuth2 server is down: %s" % exc.__class__.__name__) 90 | except HTTPError as exc: 91 | if 400 <= exc.response.status_code < 500: 92 | log.warning( 93 | "OAuth2 server does not refresh the token (%s: %s), " 94 | "force disconnect", 95 | exc.response.status_code, 96 | exc.response.text, 97 | ) 98 | elif 500 <=exc.response.status_code < 600: 99 | log.error( 100 | "OAuth2 server is not working properly? got %s", 101 | exc.response.status_code, 102 | ) 103 | except Exception: 104 | log.exception( 105 | "Unexpected exception while retrieving token using refresh token" 106 | ) 107 | return {} 108 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/settings.py: -------------------------------------------------------------------------------- 1 | """Load settings for the wagtail-oauth2 app.""" 2 | from django.conf import settings 3 | 4 | global_prefix = "OAUTH2_" 5 | 6 | 7 | def get_setting(name, default=None): 8 | """Get the settings without the prefix.""" 9 | return getattr(settings, global_prefix + name, default) 10 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/templates/login_error.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block titletag %}{% trans "Authentication Error" %}{% endblock %} 5 | {% block content %} 6 | {% trans "Authentication Error" as account_str %} 7 | {% include "wagtailadmin/shared/header.html" with title=account_str %} 8 | 9 |
10 |

{{error}}

11 |

{{error_description}}

12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gandi/wagtail-oauth2/e75689f7a72c2b1d7a634e4d65fb57e48a769a4e/src/wagtail_oauth2/tests/__init__.py -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Oauth2TestAppConfig(AppConfig): 5 | label = "wagtail_oauth2_tests" 6 | name = "wagtail_oauth2.tests" 7 | verbose_name = "Wagtail OAuht2.0 Test app" 8 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import defaultdict 3 | from io import BytesIO 4 | from typing import Any, Dict, Tuple 5 | from unittest import mock 6 | from urllib.parse import urlencode 7 | 8 | import pytest 9 | from django.contrib.auth import get_user_model 10 | from faker import Faker 11 | from requests import Response 12 | 13 | fake = Faker() 14 | 15 | 16 | @pytest.fixture(autouse=True) 17 | def enable_db_access_for_all_tests(db): 18 | pass 19 | 20 | 21 | @pytest.fixture 22 | def user_mei(): 23 | user_cls = get_user_model() 24 | try: 25 | user = user_cls.objects.get(username="mei") 26 | except user_cls.DoesNotExist: 27 | user = user_cls() 28 | user.username = "mei" 29 | user.is_staff = True 30 | user.email = "mei@toto.ro" 31 | user.first_name = "Mei" 32 | user.last_name = "Kusakabe" 33 | user.is_superuser = True 34 | user.save() 35 | return user 36 | 37 | 38 | @pytest.fixture 39 | def random_userinfo(): 40 | return { 41 | "username": f"{fake.user_name()}_{fake.pystr(4, 4)}", 42 | "is_staff": True, 43 | "email": fake.email(), 44 | "first_name": fake.first_name(), 45 | "last_name": fake.last_name(), 46 | "is_superuser": True, 47 | } 48 | 49 | 50 | API_RESPONSE: Dict[Tuple[str, str], Tuple[int, Any, Any]] = { 51 | ( 52 | "post", 53 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&code=codecode&grant_type=authorization_code", 54 | ): ( 55 | 200, 56 | # Headers 57 | {}, 58 | # Body 59 | {"access_token": "mey_accesstoken", "refresh_token": "freshmenthol"}, 60 | ), 61 | ( 62 | "post", 63 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&code=badcode&grant_type=authorization_code", 64 | ): ( 65 | 400, 66 | # Headers 67 | {}, 68 | # Body 69 | {"error": "invalid_token"}, 70 | ), 71 | ( 72 | "post", 73 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&code=e500&grant_type=authorization_code", 74 | ): ( 75 | 500, 76 | # Headers 77 | {}, 78 | # Body 79 | {"error": "server_error"}, 80 | ), 81 | ( 82 | "post", 83 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&grant_type=refresh_token&refresh_token=freshmenthol", 84 | ): ( 85 | 200, 86 | # Headers 87 | {}, 88 | # Body 89 | {"access_token": "def", "refresh_token": "freshmenthol"}, 90 | ), 91 | ( 92 | "post", 93 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&grant_type=refresh_token&refresh_token=e409", 94 | ): ( 95 | 409, 96 | # Headers 97 | {}, 98 | # Body 99 | {}, 100 | ), 101 | ( 102 | "post", 103 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&grant_type=refresh_token&refresh_token=e500", 104 | ): ( 105 | 500, 106 | # Headers 107 | {}, 108 | # Body 109 | {}, 110 | ), 111 | ( 112 | "post", 113 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&grant_type=refresh_token&refresh_token=xyz", 114 | ): ( 115 | 200, 116 | # Headers 117 | {}, 118 | # Body 119 | { 120 | "access_token": "toktok", 121 | "refresh_token": "totoro", 122 | "expires_in": 3600, 123 | }, 124 | ), 125 | } 126 | 127 | 128 | class DummyResponse(Response): 129 | """Represent a requests response.""" 130 | 131 | def __init__(self, body="", status_code=200, headers=None): 132 | super(DummyResponse, self).__init__() 133 | self.raw = BytesIO(body.encode("utf-8")) 134 | self.status_code = status_code 135 | self.headers = headers or {} 136 | 137 | 138 | class RequestsMock(mock.Mock): 139 | """A mock for request calls.""" 140 | 141 | api_calls = defaultdict(list) 142 | 143 | def __init__( 144 | self, 145 | api_response, 146 | missing_status=503, 147 | missing_body="Internal Server Error", 148 | ): 149 | api_response = api_response 150 | 151 | def fake_api(method, url, **kwargs): 152 | get_params = sorted(kwargs.get("params", {}).items()) 153 | if get_params: 154 | url += "?" + urlencode(get_params) 155 | post_params = sorted(kwargs.get("data", {}).items()) 156 | if post_params: 157 | url += "~" + urlencode(post_params) 158 | 159 | if "TimeoutError" in url: 160 | raise TimeoutError("Boom") 161 | 162 | if "ConnectionError" in url: 163 | raise ConnectionError("Boom") 164 | 165 | if "RuntimeError" in url: 166 | raise RuntimeError("Sometime things fails") 167 | 168 | if (method, url) in api_response: 169 | status, headers, res = api_response[(method, url)] 170 | res = json.dumps(res) 171 | else: 172 | print(f"{method} {url} is missing returning missing results") 173 | status, headers, res = missing_status, None, missing_body 174 | RequestsMock.api_calls[(method, url)].append(kwargs) 175 | return DummyResponse(res, status, headers) 176 | 177 | super(RequestsMock, self).__init__(side_effect=fake_api) 178 | 179 | 180 | @pytest.fixture() 181 | def mock_oauth2(): 182 | mock_req = mock.patch( 183 | "requests.Session.request", 184 | RequestsMock(API_RESPONSE), 185 | ) 186 | mock_req.start() 187 | yield RequestsMock 188 | mock_req.stop() 189 | RequestsMock.api_calls.clear() 190 | 191 | 192 | @pytest.fixture() 193 | def state(): 194 | state = fake.pystr(4, 4) 195 | with mock.patch("wagtail_oauth2.views.gen_state_name", return_value=state): 196 | yield state 197 | 198 | 199 | @pytest.fixture() 200 | def auth_code(mock_oauth2): 201 | return "codecode" 202 | 203 | 204 | class DummyRequestWithSession: 205 | def __init__(self, session): 206 | self.session = session or {} 207 | 208 | 209 | @pytest.fixture() 210 | def dummy_request_with_session(params): 211 | yield DummyRequestWithSession(params.get("session")) 212 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | from typing import Dict, Any 4 | 5 | from django.core.exceptions import PermissionDenied 6 | 7 | USERS = { 8 | "mey_accesstoken": { 9 | "username": "mei", 10 | "is_superuser": True, 11 | } 12 | } 13 | 14 | 15 | def load_userinfo(access_token): 16 | """ 17 | Load userinfo from access token. 18 | 19 | Return a dict containing username, email, first_name, last_name, and 20 | is_superuser fields to register any new user. 21 | """ 22 | try: 23 | return USERS[access_token] 24 | except KeyError: 25 | raise PermissionDenied 26 | 27 | 28 | OAUTH2_VERIFY_CERTIFICATE = False 29 | OAUTH2_TIMEOUT = 30 30 | 31 | OAUTH2_LOAD_USERINFO = load_userinfo 32 | 33 | OAUTH2_CLIENT_ID = "Mei" 34 | OAUTH2_CLIENT_SECRET = "T0t0r0" 35 | 36 | OAUTH2_AUTH_URL = "https://gandi.v5/authorize" 37 | OAUTH2_TOKEN_URL = "https://gandi.v5/token" 38 | OAUTH2_LOGOUT_URL = "https://gandi.v5/logout" 39 | 40 | 41 | ROOT_URLCONF = "wagtail_oauth2.tests.urls" 42 | 43 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 44 | SECRET_KEY = "beep" 45 | 46 | DATABASES: Dict[str, Dict[str, Any]] = { 47 | "default": { 48 | "ENGINE": "django.db.backends.sqlite3", 49 | "NAME": os.path.join(BASE_DIR, "test.db"), 50 | "TEST": { 51 | "NAME": "test.db", 52 | }, 53 | }, 54 | } 55 | 56 | 57 | INSTALLED_APPS = [ 58 | "django.contrib.auth", 59 | "django.contrib.contenttypes", 60 | "django.contrib.sessions", 61 | "wagtail_oauth2", 62 | "wagtail.admin", 63 | "wagtail.users", 64 | "wagtail.core", 65 | "tests", 66 | ] 67 | 68 | MIDDLEWARE = ["django.contrib.sessions.middleware.SessionMiddleware"] 69 | 70 | 71 | TEMPLATES = [ 72 | { 73 | "BACKEND": "django.template.backends.django.DjangoTemplates", 74 | "APP_DIRS": True, 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/test_resources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wagtail_oauth2.resources import Token 4 | 5 | 6 | def test_token_by_authcode(mock_oauth2): 7 | tokens = Token.by_authcode("codecode") 8 | assert tokens == { 9 | "access_token": "mey_accesstoken", 10 | "refresh_token": "freshmenthol", 11 | } 12 | assert mock_oauth2.api_calls == { 13 | ( 14 | "post", 15 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&code=codecode&grant_type=authorization_code", 16 | ): [ 17 | { 18 | "data": { 19 | "client_id": "Mei", 20 | "client_secret": "T0t0r0", 21 | "code": "codecode", 22 | "grant_type": "authorization_code", 23 | }, 24 | "json": None, 25 | "timeout": 30, 26 | "verify": False, 27 | } 28 | ] 29 | } 30 | 31 | 32 | @pytest.mark.parametrize("authcode", ["badcode", "e500", "TimeoutError", "ConnectionError", "RuntimeError"]) 33 | def test_token_by_authcode_unkown_user(authcode, mock_oauth2): 34 | tokens = Token.by_authcode(authcode) 35 | assert tokens == {} 36 | 37 | 38 | def test_get_authenticated_url(): 39 | url = Token.get_authenticated_url("http://my.cms", "abahoui") 40 | assert ( 41 | url 42 | == "https://gandi.v5/authorize?client_id=Mei&redirect_uri=http%3A%2F%2Fmy.cms&response_type=code&state=abahoui" 43 | ) 44 | 45 | 46 | def test_by_refresh_token(mock_oauth2): 47 | tokens = Token.by_refresh_token("freshmenthol") 48 | assert tokens == { 49 | "access_token": "def", 50 | "refresh_token": "freshmenthol", 51 | } 52 | assert mock_oauth2.api_calls == { 53 | ( 54 | "post", 55 | "https://gandi.v5/token~client_id=Mei&client_secret=T0t0r0&grant_type=refresh_token&refresh_token=freshmenthol", 56 | ): [ 57 | { 58 | "data": { 59 | "client_id": "Mei", 60 | "client_secret": "T0t0r0", 61 | "grant_type": "refresh_token", 62 | "refresh_token": "freshmenthol", 63 | }, 64 | "json": None, 65 | "timeout": 30, 66 | "verify": False, 67 | } 68 | ] 69 | } 70 | 71 | @pytest.mark.parametrize("refresh_troken", ["e409", "e500", "TimeoutError", "ConnectionError", "RuntimeError"]) 72 | def test_by_refresh_token_4xx(refresh_troken, mock_oauth2): 73 | tokens = Token.by_refresh_token(refresh_troken) 74 | assert tokens == {} 75 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | from django.test import override_settings 4 | 5 | from wagtail_oauth2.utils import get_access_token, save_tokens 6 | 7 | 8 | @mock.patch("time.time", return_value=1000) 9 | @pytest.mark.parametrize( 10 | "params", 11 | [ 12 | { 13 | "settings": {}, 14 | "tokens": {"access_token": "abc"}, 15 | "expected": {}, 16 | }, 17 | { 18 | "settings": {"OAUTH2_STORE_TOKENS": True}, 19 | "tokens": {"access_token": "abc"}, 20 | "expected": { 21 | "wagtail_oauth2_access_token": "abc", 22 | "wagtail_oauth2_expires_at": 1900, 23 | }, 24 | }, 25 | { 26 | "settings": {"OAUTH2_STORE_TOKENS": True, "OAUTH2_DEFAULT_TTL": 42}, 27 | "tokens": {"access_token": "abc"}, 28 | "expected": { 29 | "wagtail_oauth2_access_token": "abc", 30 | "wagtail_oauth2_expires_at": 1042, 31 | }, 32 | }, 33 | { 34 | "settings": {"OAUTH2_STORE_TOKENS": True, "OAUTH2_DEFAULT_TTL": 42}, 35 | "tokens": {"access_token": "abc", "expires_in": 300}, 36 | "expected": { 37 | "wagtail_oauth2_access_token": "abc", 38 | "wagtail_oauth2_expires_at": 1300, 39 | }, 40 | }, 41 | { 42 | "settings": {"OAUTH2_STORE_TOKENS": True}, 43 | "tokens": { 44 | "access_token": "abc", 45 | "refresh_token": "xyz", 46 | "expires_in": 300, 47 | }, 48 | "expected": { 49 | "wagtail_oauth2_access_token": "abc", 50 | "wagtail_oauth2_refresh_token": "xyz", 51 | "wagtail_oauth2_expires_at": 1300, 52 | }, 53 | }, 54 | ], 55 | ) 56 | def test_save_tokens(time, dummy_request_with_session, params): 57 | tokens = params["tokens"] 58 | settings = params["settings"] 59 | expected = params["expected"] 60 | with override_settings(**settings): 61 | save_tokens(dummy_request_with_session, tokens) 62 | assert dummy_request_with_session.session == expected 63 | 64 | 65 | @mock.patch("time.time", return_value=1000) 66 | @pytest.mark.parametrize( 67 | "params", 68 | [ 69 | { 70 | "settings": {"OAUTH2_STORE_TOKENS": True}, 71 | "session": { 72 | "wagtail_oauth2_access_token": "abc", 73 | "wagtail_oauth2_expires_at": 1300, 74 | }, 75 | "expected": "abc", 76 | }, 77 | { 78 | "settings": {"OAUTH2_STORE_TOKENS": True}, 79 | "session": { 80 | "wagtail_oauth2_access_token": "abc", 81 | "wagtail_oauth2_expires_at": 300, 82 | }, 83 | "expected": None, 84 | }, 85 | { 86 | "settings": {"OAUTH2_STORE_TOKENS": False}, 87 | "session": { 88 | "wagtail_oauth2_access_token": "abc", 89 | "wagtail_oauth2_expires_at": 1300, 90 | }, 91 | "expected": None, 92 | }, 93 | { 94 | # Expired Scenario 95 | "settings": { 96 | "OAUTH2_STORE_TOKENS": True, 97 | }, 98 | "session": { 99 | "wagtail_oauth2_access_token": "abc", 100 | "wagtail_oauth2_refresh_token": "xyz", 101 | "wagtail_oauth2_expires_at": 300, 102 | }, 103 | "expected": "toktok", 104 | "expected_new_session": { 105 | "wagtail_oauth2_access_token": "toktok", 106 | "wagtail_oauth2_expires_at": 4600, 107 | "wagtail_oauth2_refresh_token": "totoro", 108 | }, 109 | }, 110 | ], 111 | ) 112 | def test_get_access_token(time, mock_oauth2, dummy_request_with_session, params): 113 | settings = params["settings"] 114 | with override_settings(**settings): 115 | token = get_access_token(dummy_request_with_session) 116 | assert token == params["expected"] 117 | if "expected_new_session" in params: 118 | assert dummy_request_with_session.session == params["expected_new_session"] 119 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.contrib.auth.models import User 4 | 5 | from wagtail_oauth2.views import gen_state_name, get_cookie_name, get_user_from_userinfo 6 | 7 | 8 | def test_gen_state_name(): 9 | state1 = gen_state_name() 10 | state2 = gen_state_name() 11 | assert state1 != state2 12 | assert re.match("[0-9a-z]{10}", state1) 13 | assert re.match("[0-9a-z]{10}", state2) 14 | 15 | 16 | def test_get_cookie_name(): 17 | return get_cookie_name("plop") == "oauth2.plop" 18 | 19 | 20 | def test_get_user_from_userinfo_exists(user_mei): 21 | user = get_user_from_userinfo({"username": "mei"}) 22 | assert user is not None 23 | assert isinstance(user, User) 24 | assert user == user_mei 25 | 26 | 27 | def test_get_user_from_userinfo_does_not_exists(random_userinfo): 28 | user = get_user_from_userinfo(random_userinfo) 29 | assert user is not None 30 | assert isinstance(user, User) 31 | assert user.username == random_userinfo["username"] 32 | assert user.email == random_userinfo["email"] 33 | assert user.first_name == random_userinfo["first_name"] 34 | assert user.last_name == random_userinfo["last_name"] 35 | 36 | 37 | def test_view_get(client, state): 38 | resp = client.get("/admin/login/") 39 | assert resp.status_code == 302 40 | redir = "https%3A%2F%2Ftestserver%2Fadmin%2Flogin%2F" 41 | assert ( 42 | resp.headers["location"] 43 | == f"https://gandi.v5/authorize?client_id=Mei&redirect_uri={redir}&response_type=code&state={state}" 44 | ) 45 | state_cookie = resp.cookies[get_cookie_name(state)] 46 | assert state_cookie.value == "/admin/" 47 | 48 | 49 | def test_view_login_next(client, state): 50 | resp = client.get("/admin/login/", {"next": "/admin/page/42"}) 51 | assert resp.status_code == 302 52 | redir = "https%3A%2F%2Ftestserver%2Fadmin%2Flogin%2F" 53 | assert ( 54 | resp.headers["location"] 55 | == f"https://gandi.v5/authorize?client_id=Mei&redirect_uri={redir}&response_type=code&state={state}" 56 | ) 57 | state_cookie = resp.cookies[get_cookie_name(state)] 58 | assert state_cookie.value == "/admin/page/42" 59 | 60 | 61 | def test_view_login_next_is_login(client, state): 62 | resp = client.get("/admin/login/", {"next": "/admin/login/"}) 63 | assert resp.status_code == 302 64 | redir = "https%3A%2F%2Ftestserver%2Fadmin%2Flogin%2F" 65 | assert ( 66 | resp.headers["location"] 67 | == f"https://gandi.v5/authorize?client_id=Mei&redirect_uri={redir}&response_type=code&state={state}" 68 | ) 69 | state_cookie = resp.cookies[get_cookie_name(state)] 70 | assert state_cookie.value == "/admin/" 71 | 72 | 73 | def test_view_login_with_auth_code(client, state, auth_code, user_mei): 74 | client.cookies.load({get_cookie_name(state): "/admin/"}) 75 | resp = client.get("/admin/login/", {"code": auth_code, "state": state}) 76 | assert resp.status_code == 302 77 | assert resp.headers["Location"] == "/admin/" 78 | assert client.session['_auth_user_id'] == str(user_mei.pk) 79 | 80 | 81 | def test_view_logout(client, user_mei): 82 | client.force_login(user_mei) 83 | assert '_auth_user_id' in client.session 84 | resp = client.get("/admin/logout/") 85 | assert resp.status_code == 302 86 | assert resp.headers["Location"] == "https://gandi.v5/logout" 87 | assert '_auth_user_id' not in client.session 88 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from wagtail.admin import urls as wagtailadmin_urls 4 | from wagtail_oauth2 import urls as oauth2_urls 5 | 6 | urlpatterns = [ 7 | # /!\ must appears before admin/ to override the login part 8 | path("admin/", include(oauth2_urls)), 9 | path("admin/", include(wagtailadmin_urls)), 10 | ] 11 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/urls.py: -------------------------------------------------------------------------------- 1 | """Override wagtail views to use Gandi ID as the login provider.""" 2 | from django.urls import re_path 3 | 4 | from .views import Oauth2LoginView, Oauth2LogoutView 5 | 6 | 7 | urlpatterns = [ 8 | re_path(r"^login/$", Oauth2LoginView.as_view(), name="wagtailadmin_login"), 9 | re_path(r"^logout/$", Oauth2LogoutView.as_view(), name="wagtailadmin_logout"), 10 | ] 11 | """Url to load in the app.""" 12 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import cast 3 | 4 | from .settings import get_setting 5 | from .resources import Token 6 | 7 | DEFAULT_SESSION_KEY_PREFIX = "wagtail_oauth2_" 8 | 9 | 10 | def save_tokens(request, tokens): 11 | if not get_setting("STORE_TOKENS", False): 12 | return None 13 | prefix = get_setting("SESSION_KEY_PREFIX", DEFAULT_SESSION_KEY_PREFIX) 14 | request.session[f"{prefix}access_token"] = tokens["access_token"] 15 | if "refresh_token" in tokens: 16 | request.session[f"{prefix}refresh_token"] = tokens["refresh_token"] 17 | if "expires_in" in tokens: 18 | request.session[f"{prefix}expires_at"] = int(time.time() + tokens["expires_in"]) 19 | else: 20 | request.session[f"{prefix}expires_at"] = int( 21 | time.time() + cast(float, get_setting("DEFAULT_TTL", 15 * 60)) # 15 minutes 22 | ) 23 | 24 | 25 | def get_access_token(request): 26 | """Get the access token, or fetch a new one if it is possible, otherwise return None.""" 27 | if not get_setting("STORE_TOKENS", False): 28 | return None 29 | 30 | prefix = get_setting("SESSION_KEY_PREFIX", DEFAULT_SESSION_KEY_PREFIX) 31 | access_token = request.session.get(f"{prefix}access_token") 32 | refresh_token = request.session.get(f"{prefix}refresh_token") 33 | expires_at = int(request.session.get(f"{prefix}expires_at", 0)) 34 | 35 | if access_token and expires_at > time.time(): 36 | return access_token 37 | 38 | if refresh_token: 39 | tokens = Token.by_refresh_token(refresh_token) 40 | if tokens: 41 | save_tokens(request, tokens) 42 | return tokens["access_token"] 43 | -------------------------------------------------------------------------------- /src/wagtail_oauth2/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Views to log in using an OAuth2.0 Authorization Server. 3 | 4 | Currently, we do not keep the access and refresh token in the session, 5 | because it is not necessary. 6 | 7 | It could be done later to retrieve data from API using OAuth2 authorizations. 8 | """ 9 | import binascii 10 | import logging 11 | import os 12 | from typing import cast 13 | 14 | from django.conf import settings 15 | from django.contrib.auth import get_user_model, login as auth_login 16 | from django.contrib.auth.base_user import AbstractBaseUser 17 | from django.contrib.auth.models import Group 18 | from django.shortcuts import redirect 19 | from django.urls import reverse 20 | from wagtail.admin.views.account import LoginView, LogoutView 21 | from wagtail_oauth2.utils import save_tokens 22 | 23 | from .resources import Token 24 | from .settings import get_setting 25 | 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | def gen_state_name(): 31 | """Generate a random state name.""" 32 | return binascii.hexlify(os.urandom(5)).decode("ascii") 33 | 34 | 35 | def get_cookie_name(state): 36 | """Generate the cookie name for the OAuth2.0 state.""" 37 | return f"oauth.{state}" 38 | 39 | 40 | class StateError(ValueError): 41 | """raised in case the oauth2 state workflow is not valid.""" 42 | 43 | 44 | def get_user_from_userinfo(userinfo) -> AbstractBaseUser: 45 | """Create or retrieve a user from the wagtail point of view, from the userinfo.""" 46 | username = userinfo["username"] 47 | user_cls = get_user_model() 48 | try: 49 | user = user_cls.objects.get(username=username) 50 | except user_cls.DoesNotExist: 51 | # Create a new user. There's no need to set a password 52 | # because only the password from settings.py is checked. 53 | user = user_cls(username=username) 54 | 55 | user.is_staff = userinfo.get("is_staff", True) 56 | user.email = userinfo.get("email") 57 | user.first_name = userinfo.get("first_name") 58 | user.last_name = userinfo.get("last_name") 59 | user.is_superuser = userinfo["is_superuser"] 60 | user.save() 61 | if not user.is_superuser: 62 | groups = userinfo.get("groups", ["Moderators", "Editors"]) 63 | for group_name in groups: 64 | group = Group.objects.filter(name=group_name).first() 65 | group.user_set.add(user) 66 | group.save() 67 | return user 68 | 69 | 70 | class Oauth2LoginView(LoginView): 71 | """Login view.""" 72 | 73 | template_name = "login_error.html" 74 | 75 | def add_state(self, request, resp, state): 76 | """Set the OAuth2.0 state in the cookie.""" 77 | default = "/admin" 78 | if getattr(settings, "WAGTAIL_APPEND_SLASH", True): 79 | default += "/" 80 | referrer = request.GET.get("next", self.get_success_url()) 81 | # never use the login form itself as referrer 82 | if referrer.startswith(reverse("wagtailadmin_login")): 83 | referrer = default 84 | if not referrer.startswith("/"): 85 | referrer = default 86 | 87 | host = request.get_host() 88 | resp.set_cookie( 89 | get_cookie_name(state), 90 | referrer, 91 | max_age=300, 92 | secure=not host.startswith("localhost:"), 93 | httponly=True, 94 | ) 95 | return resp 96 | 97 | def check_state(self, request): 98 | """Verify that the OAuth2.0 state in the cookie match callback query.""" 99 | state = request.GET.get("state") 100 | if not state: 101 | raise StateError("Missing OAuth2 state") 102 | cookie_name = get_cookie_name(state) 103 | if cookie_name not in request.COOKIES: 104 | raise StateError("Invalid OAuth2 state") 105 | return request.COOKIES[cookie_name] 106 | 107 | def start_oauth2_dance(self, request, state): 108 | """Create the http query that redirect to the OAuth2.0""" 109 | log.info("Redirect to the oauth2 authorization server") 110 | host = request.get_host() 111 | # XXX request.scheme does not works properly, maybe a config issue 112 | scheme = "http" if host.startswith("localhost:") else "https" 113 | url = Token.get_authenticated_url( 114 | scheme + "://" + host + reverse("wagtailadmin_login"), 115 | state, 116 | ) 117 | return redirect(url) 118 | 119 | def consume_oauth2_code(self, request, redirect_uri): 120 | """Retrieve a bearer token from the authorization code, log the user.""" 121 | log.info("Consume the code") 122 | token = Token.by_authcode(request.GET["code"]) 123 | load_user = get_setting("LOAD_USERINFO") 124 | if load_user is None: 125 | raise RuntimeError("Missing configuration OAUTH2_LOAD_USERINFO") 126 | userinfo = load_user(token["access_token"]) 127 | user = get_user_from_userinfo(userinfo) 128 | auth_login(request, user) 129 | if get_setting("STORE_TOKENS", False): 130 | save_tokens(request, token) 131 | return redirect(redirect_uri) 132 | 133 | def render_error(self, error, error_description): 134 | """Render the template to diplay error to the user.""" 135 | return self.render_to_response( 136 | { 137 | "error": error, 138 | "error_description": error_description, 139 | } 140 | ) 141 | 142 | def get(self, request, *args, **kwargs): 143 | """Handle HTTP GET query.""" 144 | redirect_uri = "" 145 | if "code" in request.GET or "error" in request.GET: 146 | try: 147 | redirect_uri = self.check_state(request) 148 | except StateError as exc: 149 | return self.render_error("OAuth 2 Security Error", str(exc)) 150 | 151 | if "code" in request.GET: 152 | return self.consume_oauth2_code(request, redirect_uri) 153 | elif "error" in request.GET: 154 | log.info("OAuth2 server error") 155 | return self.render_error( 156 | request.GET["error"], 157 | request.GET.get("error_description"), 158 | ) 159 | else: 160 | state = gen_state_name() 161 | return self.add_state( 162 | request, self.start_oauth2_dance(request, state), state 163 | ) 164 | 165 | 166 | class Oauth2LogoutView(LogoutView): 167 | """Logout view.""" 168 | 169 | def dispatch(self, request, *args, **kwargs): 170 | """Handle every HTTP Query to logout user.""" 171 | 172 | logout_url = get_setting("LOGOUT_URL") 173 | if logout_url: 174 | resp = redirect(logout_url) 175 | else: 176 | resp = super().dispatch(request, *args, **kwargs) 177 | 178 | # XXX sthis code code has been copy from Wagtail. 179 | 180 | # By default, logging out will generate a fresh sessionid cookie. We want to use the 181 | # absence of sessionid as an indication that front-end pages are being viewed by a 182 | # non-logged-in user and are therefore cacheable, so we forcibly delete the cookie here. 183 | resp.delete_cookie( 184 | settings.SESSION_COOKIE_NAME, 185 | domain=settings.SESSION_COOKIE_DOMAIN, 186 | path=settings.SESSION_COOKIE_PATH, 187 | ) 188 | 189 | # HACK: pretend that the session hasn't been modified, so that SessionMiddleware 190 | # won't override the above and write a new cookie. 191 | self.request.session.modified = False 192 | return resp 193 | --------------------------------------------------------------------------------