├── .bumpversion.cfg ├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .flake8 ├── .gitattributes ├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE.md ├── .gitignore ├── .isort.cfg ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docker-compose.yml ├── docs ├── Makefile ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat └── usage.rst ├── pytest.ini ├── reports └── .gitkeep ├── rest_framework_recaptcha ├── __init__.py ├── compat.py ├── conf.py ├── fields.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── fr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po └── validators.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── unit │ ├── __init__.py │ ├── drf_recaptcha_field │ ├── __init__.py │ ├── test_fields.py │ └── test_validators.py │ └── settings.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = true 4 | tag = true 5 | 6 | [bumpversion:file:rest_framework_recaptcha/__init__.py] 7 | search = __version__ = "{current_version}" 8 | replace = __version__ = "{new_version}" 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = rest_framework_recaptcha 3 | branch = true 4 | 5 | [report] 6 | exclude_lines = 7 | raise NotImplementedError 8 | omit = 9 | tests/* 10 | *.tox* 11 | *conftest* 12 | *compat.py 13 | setup.py 14 | show_missing = true 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.bumpversion.cfg 3 | !.coveragerc 4 | !.flake8 5 | docs 6 | reports 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [*.py] 18 | line_length = 79 19 | 20 | [*.{html,css,scss,json,yml,yaml}] 21 | indent_size = 2 22 | 23 | [*.md] 24 | trim_trailing_whitespace = false 25 | 26 | [Makefile] 27 | indent_style = tab 28 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git 4 | docs/conf.py 5 | build 6 | dist 7 | .tox 8 | max-line-length = 79 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Maximilien-R 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Django REST framework reCAPTCHA version: 2 | * Django REST framework version: 3 | * Django version: 4 | * Python version: 5 | * Operating System: 6 | 7 | ### Description 8 | 9 | Describe what you were trying to get done. 10 | Tell us what happened, what went wrong, and what you expected to happen. 11 | 12 | ### What I Did 13 | 14 | ``` 15 | Paste the command(s) you ran and the output. 16 | If there was a crash, please include the traceback here. 17 | ``` 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # Reports 106 | reports/* 107 | !reports/.gitkeep 108 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=79 3 | indent=' ' 4 | multi_line_output=3 5 | known_third_party=djangorestframework,django-appconf,django-ipware,pytz 6 | known_first_party=rest_framework_recaptcha,tests 7 | known_django=django 8 | sections=FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER 9 | include_trailing_comma=1 10 | use_parentheses=1 11 | force_grid_wrap=0 12 | combine_as_imports=1 13 | lines_between_types=1 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | cache: pip 6 | 7 | .mixins: 8 | - &xenial-mixin 9 | os: linux 10 | dist: xenial 11 | sudo: true 12 | 13 | install: 14 | - pip install tox pyOpenSSL 15 | - make set-alpha-version 16 | 17 | script: 18 | - tox 19 | 20 | matrix: 21 | fast_finish: true 22 | include: 23 | - python: 3.6 24 | env: TOXENV=style 25 | - python: 3.6 26 | env: TOXENV=complexity 27 | - python: 3.6 28 | env: TOXENV=security 29 | 30 | - python: 2.7 31 | env: TOXENV=drf34-py27-django110 32 | - python: 2.7 33 | env: TOXENV=drf35-py27-django110 34 | - python: 2.7 35 | env: TOXENV=drf36-py27-django110 36 | - python: 2.7 37 | env: TOXENV=drf37-py27-django110 38 | - python: 2.7 39 | env: TOXENV=drf38-py27-django110 40 | - python: 2.7 41 | env: TOXENV=drf39-py27-django110 42 | - python: 2.7 43 | env: TOXENV=drf37-py27-django111 44 | - python: 2.7 45 | env: TOXENV=drf38-py27-django111 46 | - python: 2.7 47 | env: TOXENV=drf39-py27-django111 48 | 49 | - python: 3.4 50 | env: TOXENV=drf34-py34-django110 51 | - python: 3.4 52 | env: TOXENV=drf35-py34-django110 53 | - python: 3.4 54 | env: TOXENV=drf36-py34-django110 55 | - python: 3.4 56 | env: TOXENV=drf37-py34-django110 57 | - python: 3.4 58 | env: TOXENV=drf38-py34-django110 59 | - python: 3.4 60 | env: TOXENV=drf39-py34-django110 61 | - python: 3.4 62 | env: TOXENV=drf37-py34-django111 63 | - python: 3.4 64 | env: TOXENV=drf38-py34-django111 65 | - python: 3.4 66 | env: TOXENV=drf39-py34-django111 67 | - python: 3.4 68 | env: TOXENV=drf37-py34-django20 69 | - python: 3.4 70 | env: TOXENV=drf38-py34-django20 71 | - python: 3.4 72 | env: TOXENV=drf39-py34-django20 73 | 74 | - python: 3.5 75 | env: TOXENV=drf34-py35-django110 76 | - python: 3.5 77 | env: TOXENV=drf35-py35-django110 78 | - python: 3.5 79 | env: TOXENV=drf36-py35-django110 80 | - python: 3.5 81 | env: TOXENV=drf37-py35-django110 82 | - python: 3.5 83 | env: TOXENV=drf38-py35-django110 84 | - python: 3.5 85 | env: TOXENV=drf39-py35-django110 86 | - python: 3.5 87 | env: TOXENV=drf37-py35-django111 88 | - python: 3.5 89 | env: TOXENV=drf38-py35-django111 90 | - python: 3.5 91 | env: TOXENV=drf39-py35-django111 92 | - python: 3.5 93 | env: TOXENV=drf37-py35-django20 94 | - python: 3.5 95 | env: TOXENV=drf38-py35-django20 96 | - python: 3.5 97 | env: TOXENV=drf39-py35-django20 98 | - python: 3.5 99 | env: TOXENV=drf37-py35-django21 100 | - python: 3.5 101 | env: TOXENV=drf38-py35-django21 102 | - python: 3.5 103 | env: TOXENV=drf39-py35-django21 104 | 105 | - python: 3.6 106 | env: TOXENV=drf37-py36-django110 107 | - python: 3.6 108 | env: TOXENV=drf38-py36-django110 109 | - python: 3.6 110 | env: TOXENV=drf39-py36-django110 111 | - python: 3.6 112 | env: TOXENV=drf37-py36-django111 113 | - python: 3.6 114 | env: TOXENV=drf38-py36-django111 115 | - python: 3.6 116 | env: TOXENV=drf39-py36-django111 117 | - python: 3.6 118 | env: TOXENV=drf37-py36-django20 119 | - python: 3.6 120 | env: TOXENV=drf38-py36-django20 121 | - python: 3.6 122 | env: TOXENV=drf39-py36-django20 123 | - python: 3.6 124 | env: TOXENV=drf37-py36-django21 125 | - python: 3.6 126 | env: TOXENV=drf38-py36-django21 127 | - python: 3.6 128 | env: TOXENV=drf39-py36-django21 129 | 130 | - <<: *xenial-mixin 131 | python: 3.7 132 | env: TOXENV=drf37-py37-django110 133 | - <<: *xenial-mixin 134 | python: 3.7 135 | env: TOXENV=drf38-py37-django110 136 | - <<: *xenial-mixin 137 | python: 3.7 138 | env: TOXENV=drf39-py37-django110 139 | - <<: *xenial-mixin 140 | python: 3.7 141 | env: TOXENV=drf37-py37-django111 142 | - <<: *xenial-mixin 143 | python: 3.7 144 | env: TOXENV=drf38-py37-django111 145 | - <<: *xenial-mixin 146 | python: 3.7 147 | env: TOXENV=drf39-py37-django111 148 | - <<: *xenial-mixin 149 | python: 3.7 150 | env: TOXENV=drf37-py37-django20 151 | - <<: *xenial-mixin 152 | python: 3.7 153 | env: TOXENV=drf38-py37-django20 154 | - <<: *xenial-mixin 155 | python: 3.7 156 | env: TOXENV=drf39-py37-django20 157 | - <<: *xenial-mixin 158 | python: 3.7 159 | env: TOXENV=drf37-py37-django21 160 | - <<: *xenial-mixin 161 | python: 3.7 162 | env: TOXENV=drf38-py37-django21 163 | - <<: *xenial-mixin 164 | python: 3.7 165 | env: TOXENV=drf39-py37-django21 166 | - stage: deploy alpha version 167 | if: type != pull_request AND branch != master 168 | python: 3.6 169 | script: skip 170 | deploy: 171 | provider: pypi 172 | server: https://test.pypi.org/legacy/ 173 | user: Maximilien-R 174 | password: 175 | secure: M/RF3Lo5tB0lw1Whli3k8MLoPBQYF5v0rCiLsNqsvSIt/uy7AAIfL+zZs9Tkjo7wFVf5jACahBlVqcnmqlaKVKh++ep2SAUO4vCArIkNco2vqHV8TQTjc6wGKcZwC1GIRqNYnCM9XAUrq4sRE3Tcve4yMOGlirUG1zEn+ZS6J2Hbqk/jWJAfc9LT7r1gYkExWUItqhj6NxcN43wEsrvRwqM1cP2dJGC/tLQdK/mMDk6Cp6ODIYAKIzMOHjX87fUI6CzOsP4H4MdXGUPOfzciFWhdNoyC9I64xZwSil3KD8bY6mbsldh+zPiffYZEB1eRAzBeJpiQmFP6WjlexFFrg9LJiGBh1a06PiO1Rt4HdE1w+EmJBhA2Crh/l0O1x5cMooOZEXV4/ztlGCqQ1Js1MAaPY2uWwRoNENx8gVlovTDfL1bp9t75i69K+pXvHyHfYW+Q14OYb3yNfL2O2O6SumXmWJTysCQBc9rYo2YjrdQo6r09KHanA13w1leh+NmKX5FgT4ivZC3EN6W3tN3MYvt8cyv7fmYtC9CWWVARxgcQy4nn28A/cnheuQTQ1BseVfmhGPJNiXeKsr+pKSXRrR77Y6jP1+iT5HXnGdeBlBbuMCK3QDUQqN/f3Ro9wGSZ1YKurDmrI6A5MOmRnG68ZXbixg0x6gFaxA8e4IaEH2I= 176 | skip_cleanup: true 177 | on: 178 | all_branches: true 179 | condition: "$TRAVIS_BRANCH != master" 180 | - stage: deploy production version 181 | if: type != pull_request AND branch = master 182 | python: 3.6 183 | script: skip 184 | deploy: 185 | provider: pypi 186 | user: Maximilien-R 187 | password: 188 | secure: ZsvPA4Q1m3SF5HiJD3cWwySUCKihxlL4l57z3WxCYkXRmVc082DPooum5Wnqy57QXPj2Y6yxIevMBA0Z7MX/nRtqtvmz3b5CH01FWjHtMKb12oiXj5EjwwYSXHyasOBPNELnvB6wVhYDcdQreCOetYwSsnlfAKk2DkPe2T6i1fIVJ3C0MUZ5Tm2ua1ehsNTyxuDRAUz5yjPxlPi5Jzx2oQs0JxUOj/y+YPEfeQUfNl8dOY2JWXlPkQFpoRsFSKJ90anb2dcdAESR4YJD4V4Mp9r/lX1ITi+krY61kplLiZKN6davisIRltyFmO8diXdFWMeHrKfKPnFd6b2So7FBCiA1D3dxvXedkgWhH618zsc2C7+glTmUsPubxNBl8ClofvvWNUZQ/15+HVlCKr7ei9EaBKLUjtlrwNFcq6HVGc9K+YuFszfbvWLRr6z/+vUbI8HS0TX7AMRlq6MqYLO6T0tPCEsZzWToBrU08xINDWN3DJ1Sg0naJ0dL3hym0y/td9ELw6XzVQMRLsWfmGBFhojNnZp/ZwH5sg1tNTRFtDXLGrPJQAsDYfXeP5fLEJc0IakoSHsb0vZr7JA9aHWOXLiY3WrjDLLe+f0gy2vGXkecwPewsbOw87/ZzKM+GX7TA/rRsFzxF1L1vcCfIWzwwJn+7djw++aYWxUvvAFboAc= 189 | on: 190 | branch: master 191 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | Development Lead 5 | ---------------- 6 | 7 | * Maximilien Raulic 8 | 9 | Contributors 10 | ------------ 11 | 12 | None yet. Why not be the first? 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions are welcome, and they are greatly appreciated! Every little bit 5 | helps, and credit will always be given. 6 | 7 | You can contribute in many ways: 8 | 9 | Types of Contributions 10 | ---------------------- 11 | 12 | Report Bugs 13 | ~~~~~~~~~~~ 14 | 15 | Report bugs at https://github.com/Maximilien-R/django-rest-framework-recaptcha/issues. 16 | 17 | If you are reporting a bug, please include: 18 | 19 | * Your operating system name and version. 20 | * Any details about your local setup that might be helpful in troubleshooting. 21 | * Detailed steps to reproduce the bug. 22 | 23 | Fix Bugs 24 | ~~~~~~~~ 25 | 26 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 27 | wanted" is open to whoever wants to implement it. 28 | 29 | Implement Features 30 | ~~~~~~~~~~~~~~~~~~ 31 | 32 | Look through the GitHub issues for features. Anything tagged with "enhancement" 33 | and "help wanted" is open to whoever wants to implement it. 34 | 35 | Write Documentation 36 | ~~~~~~~~~~~~~~~~~~~ 37 | 38 | Django REST framework reCAPTCHA could always use more documentation, whether 39 | as part of the official Django REST framework reCAPTCHA docs, in docstrings, or 40 | even on the web in blog posts, articles, and such. 41 | 42 | Submit Feedback 43 | ~~~~~~~~~~~~~~~ 44 | 45 | The best way to send feedback is to file an issue at 46 | https://github.com/Maximilien-R/django-rest-framework-recaptcha/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? The easiest way to work on 59 | ``djangorestframework-recaptcha`` for local development is to use Docker. 60 | ``djangorestframework-recaptcha`` come with a ``Dockerfile`` and a ``Makefile`` 61 | that implements all the needed stuff and helpers to work on this project. 62 | 63 | 1. Install `Docker `_. 64 | 2. Fork the ``djangorestframework-recaptcha`` 65 | `GitHub repository `_. 66 | 3. Clone your fork locally: 67 | 68 | .. code-block:: console 69 | 70 | $ git clone git@github.com:your_name_here/django-rest-framework-recaptcha.git 71 | 72 | 4. Create a branch for local development: 73 | 74 | .. code-block:: console 75 | 76 | $ git checkout -b name-of-your-bugfix-or-feature 77 | 78 | Now you can make your changes locally. 79 | 80 | 5. When you're done making changes, check that your changes pass format check, 81 | flake8, complexity and the tests, including testing other Python versions 82 | with tox: 83 | 84 | .. code-block:: console 85 | 86 | $ make check-format 87 | $ make style 88 | $ make complexity 89 | $ make test-all 90 | 91 | 6. Format your code and then commit your changes and push your branch to 92 | GitHub: 93 | 94 | .. code-block:: console 95 | 96 | $ make format 97 | $ git add . 98 | $ git commit -m "Your detailed description of your changes." 99 | $ git push origin name-of-your-bugfix-or-feature 100 | 101 | 7. Submit a pull request through the GitHub website. 102 | 103 | Pull Request Guidelines 104 | ----------------------- 105 | 106 | Before you submit a pull request, check that it meets these guidelines: 107 | 108 | 1. The pull request should include tests. 109 | 2. If the pull request adds functionality, the docs should be updated. Put 110 | your new functionality into a function with a docstring. 111 | 3. The pull request should work for Python 2.7, 3.4, 3.5, 3.6 and 3.7, and for PyPy. 112 | Check https://travis-ci.org/Maximilien-R/django-rest-framework-recaptcha/pull_requests 113 | and make sure that the tests pass for all supported Python versions. 114 | 4. Your code need to be formatted. On this project we use the 115 | `black `_ code formatter. You can easily 116 | format your code with this command: ``make format``. 117 | 118 | Deploying 119 | --------- 120 | 121 | A reminder for the maintainers on how to deploy. 122 | Make sure all your changes are committed (including an entry in 123 | ``HISTORY.rst``). Then run: 124 | 125 | .. code-block:: console 126 | 127 | $ make bumpversion -e VERSION_PART=patch # options: major / minor / patch 128 | $ git push 129 | $ git push --tags 130 | 131 | Travis will then deploy to PyPI if tests pass. 132 | 133 | Translations 134 | ------------ 135 | 136 | You can also participate in the project by adding new language or improving 137 | translations. 138 | 139 | To add a new language: 140 | 141 | .. code-block:: console 142 | 143 | $ make build-translations -e LOCALE=en 144 | 145 | To update available languages and check for new strings: 146 | 147 | .. code-block:: console 148 | 149 | $ make update-translations 150 | 151 | To compile translations: 152 | 153 | .. code-block:: console 154 | 155 | $ make compile-translations 156 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.5 2 | 3 | RUN apt-get update && apt-get install -y gettext 4 | 5 | RUN mkdir -p /usrc/src/app 6 | WORKDIR /usr/src/app 7 | 8 | ENV PYTHONPATH=/usr/src/app 9 | 10 | COPY setup.cfg . 11 | COPY setup.py . 12 | COPY README.rst . 13 | COPY HISTORY.rst . 14 | COPY rest_framework_recaptcha/__init__.py rest_framework_recaptcha/__init__.py 15 | 16 | ARG target=. 17 | 18 | RUN pip install -e "${target}" 19 | 20 | COPY . . 21 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ======= 5 | 6 | [Unreleased] 7 | ------------ 8 | 9 | Added 10 | ~~~~~ 11 | 12 | * Update dependency packages versions. 13 | * Formats Python imports with ``isort``. 14 | * Add SAST through ``bandit``. 15 | * Add dependency scan through ``safety``. 16 | * Deployment automation of an alpha package version on test PyPI for each branch (except ``master``). 17 | * Deployment automation of an production package version on test PyPI for ``master`` branch. 18 | 19 | 0.2.0 (2018-12-21) 20 | ------------------ 21 | 22 | Added 23 | ~~~~~ 24 | 25 | * Django REST framework 3.9, Python 3.7 & Django 2.1. 26 | * Set long description content type to reStructuredText. 27 | 28 | 0.1.0 (2018-07-02) 29 | ------------------ 30 | 31 | Added 32 | ~~~~~ 33 | 34 | * First release on PyPI. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018, Maximilien Raulic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | recursive-include docs *.rst make.bat *.jpg *.png *.gif 7 | recursive-include rest_framework_recaptcha/locale *.mo 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | recursive-exclude tests * 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SET_ALPHA_VERSION = 0 2 | PKG_VERSION := $(shell cat rest_framework_recaptcha/__init__.py | grep "__version__ = " | egrep -o "([0-9]+\\.[0-9]+\\.[0-9]+)") 3 | ifneq ($(and $(TRAVIS_BRANCH),$(TRAVIS_BUILD_NUMBER)),) 4 | ifneq ($(TRAVIS_BRANCH), master) 5 | PKG_VERSION := $(shell echo | awk -v pkg_version="$(PKG_VERSION)" -v travis_build_number="$(TRAVIS_BUILD_NUMBER)" '{print pkg_version "a" travis_build_number}') 6 | SET_ALPHA_VERSION = 1 7 | endif 8 | endif 9 | 10 | SHELL = /bin/sh 11 | 12 | COMPOSE = docker-compose -p rest_framework_recaptcha 13 | 14 | .PHONY: check-format 15 | check-format: 16 | $(COMPOSE) build check-format-imports 17 | $(COMPOSE) run check-format-imports 18 | $(COMPOSE) build check-format 19 | $(COMPOSE) run check-format 20 | 21 | .PHONY: format 22 | format: 23 | $(COMPOSE) build format-imports 24 | $(COMPOSE) run format-imports 25 | $(COMPOSE) build format 26 | $(COMPOSE) run format 27 | 28 | .PHONY: style 29 | style: check-format 30 | $(COMPOSE) build style 31 | $(COMPOSE) run style 32 | 33 | .PHONY: complexity 34 | complexity: 35 | $(COMPOSE) build complexity 36 | $(COMPOSE) run complexity 37 | 38 | .PHONY: test-unit 39 | test-unit: 40 | $(COMPOSE) build test-unit 41 | $(COMPOSE) run test-unit 42 | 43 | .PHONY: test 44 | test: test-unit 45 | 46 | .PHONY: test-all 47 | test-all: 48 | $(COMPOSE) build test-all 49 | $(COMPOSE) run test-all 50 | 51 | .PHONY: security-sast 52 | security-sast: 53 | $(COMPOSE) build security-sast 54 | $(COMPOSE) run security-sast 55 | 56 | .PHONY: security-dependency-scan 57 | security-dependency-scan: 58 | $(COMPOSE) build security-dependency-scan 59 | $(COMPOSE) run security-dependency-scan 60 | 61 | .PHONY: security 62 | security: security-sast security-dependency-scan 63 | 64 | .PHONY: down 65 | down: 66 | $(COMPOSE) down --volumes --rmi=local 67 | 68 | .PHONY: clean-docs 69 | clean-docs: 70 | @rm -f docs/rest_framework_recaptcha.rst 71 | @rm -f docs/modules.rst 72 | @rm -rf docs/_build 73 | 74 | .PHONY: docs 75 | docs: clean-docs 76 | $(COMPOSE) build build-docs 77 | $(COMPOSE) run build-docs 78 | 79 | .PHONY: get-version 80 | get-version: 81 | @bash -c "cat rest_framework_recaptcha/__init__.py | grep \"__version__ = \" | egrep -o \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" 82 | 83 | .PHONY: set-alpha-version 84 | set-alpha-version: 85 | ifneq ($(SET_ALPHA_VERSION), 0) 86 | bash -c "sed -i \"s@__version__[ ]*=[ ]*[\\\"\'][0-9]\+\\.[0-9]\+\\.[0-9]\+[\\\"\'].*@__version__ = \\\"$(PKG_VERSION)\\\"@\" rest_framework_recaptcha/__init__.py" 87 | endif 88 | 89 | .PHONY: bumpversion 90 | bumpversion: 91 | $(COMPOSE) build bumpversion-package 92 | $(COMPOSE) run bumpversion-package 93 | 94 | .PHONY: clean-pyc 95 | clean-pyc: 96 | @find . -name "*.pyc" -exec rm -f {} + 97 | @find . -name "*.pyo" -exec rm -f {} + 98 | @find . -name "*~" -exec rm -f {} + 99 | @find . -name __pycache__ -exec rm -rf {} + 100 | 101 | .PHONY: clean-build 102 | clean-build: 103 | @rm -rf build/ 104 | @rm -rf dist/ 105 | @rm -rf .eggs/ 106 | @find . -name "*.egg-info" -exec rm -fr {} + 107 | @find . -name "*.egg" -exec rm -f {} + 108 | 109 | .PHONY: clean 110 | clean: clean-pyc clean-build 111 | 112 | .PHONY: build 113 | build: clean compile-translations 114 | $(COMPOSE) build build-package 115 | $(COMPOSE) run build-package 116 | 117 | .PHONY: publish-test 118 | publish-test: build 119 | $(COMPOSE) build publish-test-package 120 | $(COMPOSE) run publish-test-package 121 | 122 | .PHONY: publish 123 | publish: build 124 | $(COMPOSE) build publish-package 125 | $(COMPOSE) run publish-package 126 | 127 | .PHONY: generate-translations 128 | generate-translations: 129 | ifndef LOCALE 130 | $(error LOCALE is undefined) 131 | endif 132 | $(COMPOSE) build generate-translations 133 | $(COMPOSE) run generate-translations 134 | 135 | .PHONY: update-translations 136 | update-translations: 137 | $(COMPOSE) build update-translations 138 | $(COMPOSE) run update-translations 139 | 140 | .PHONY: compile-translations 141 | compile-translations: 142 | $(COMPOSE) build compile-translations 143 | $(COMPOSE) run compile-translations 144 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Django REST framework reCAPTCHA 3 | =============================== 4 | 5 | .. image:: https://badge.fury.io/py/djangorestframework-recaptcha.svg 6 | :target: https://badge.fury.io/py/djangorestframework-recaptcha 7 | 8 | .. image:: https://travis-ci.org/Maximilien-R/django-rest-framework-recaptcha.svg?branch=master 9 | :target: https://travis-ci.org/Maximilien-R/django-rest-framework-recaptcha 10 | 11 | .. image:: https://coveralls.io/repos/github/Maximilien-R/django-rest-framework-recaptcha/badge.svg?branch=master 12 | :target: https://coveralls.io/github/Maximilien-R/django-rest-framework-recaptcha?branch=master 13 | 14 | .. image:: https://readthedocs.org/projects/django-rest-framework-recaptcha/badge/?version=latest 15 | :target: https://django-rest-framework-recaptcha.readthedocs.io/en/latest/?badge=latest 16 | :alt: Documentation Status 17 | 18 | Django REST framework reCAPTCHA provides you a serializer field to handle and 19 | validate Google reCAPTCHA response. 20 | 21 | Documentation 22 | ------------- 23 | 24 | The full documentation is at https://django-rest-framework-recaptcha.readthedocs.io. 25 | 26 | Requirements 27 | ------------ 28 | 29 | * Python: 2.7, 3.4, 3.5, 3.6, 3.7 30 | * Django: 1.10, 1.11, 2.0, 2.1 31 | * Django REST framework: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 32 | 33 | Installation 34 | ------------ 35 | 36 | To install Django REST framework reCAPTCHA, run this command in your terminal: 37 | 38 | .. code-block:: console 39 | 40 | $ pip install djangorestframework-recaptcha 41 | 42 | This is the preferred method to install Django REST framework reCAPTCHA, as it 43 | will always install the most recent stable release. 44 | 45 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 46 | you through the process. 47 | 48 | .. _pip: https://pip.pypa.io 49 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 50 | 51 | Once the ``djangorestframework-recaptcha`` installed, add it to your 52 | ``INSTALLED_APPS``: 53 | 54 | .. code-block:: python 55 | 56 | INSTALLED_APPS = ( 57 | ... 58 | "rest_framework_recaptcha", 59 | ... 60 | ) 61 | 62 | Next, register yourself and obtain your reCAPTCHA credentials at 63 | https://www.google.com/recaptcha/admin. 64 | 65 | Finally, copy/paste your Google reCAPTCHA secret key to the 66 | ``DRF_RECAPTCHA_SECRET_KEY`` setting: 67 | 68 | .. code-block:: python 69 | 70 | DRF_RECAPTCHA_SECRET_KEY = "" 71 | 72 | Usage 73 | ----- 74 | 75 | To use Django REST framework reCAPTCHA within your project you'll need to 76 | import and add the ``ReCaptchaField`` serializer field into the wanted 77 | serializer. For example: 78 | 79 | .. code-block:: python 80 | 81 | from rest_framework import serializers 82 | from rest_framework_recaptcha import ReCaptchaField 83 | 84 | 85 | class MySerializer(serializers.Serializer): 86 | recaptcha = ReCaptchaField() 87 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | package: &package 5 | build: . 6 | 7 | package-volumes: &package-volumes 8 | <<: *package 9 | volumes: 10 | - .:/usr/src/app 11 | 12 | dev-package: &dev-package 13 | build: 14 | context: . 15 | args: 16 | - target=.[development] 17 | 18 | dev-package-volumes: &dev-package-volumes 19 | <<: *dev-package 20 | volumes: 21 | - .:/usr/src/app 22 | 23 | format-imports: 24 | <<: *dev-package-volumes 25 | command: isort -rc rest_framework_recaptcha/. 26 | 27 | check-format-imports: 28 | <<: *dev-package 29 | command: isort --check-only -rc rest_framework_recaptcha/. 30 | 31 | format: 32 | <<: *dev-package-volumes 33 | command: black -l 79 --py36 rest_framework_recaptcha 34 | 35 | check-format: 36 | <<: *dev-package 37 | command: black -l 79 --py36 --check rest_framework_recaptcha 38 | 39 | style: 40 | <<: *dev-package 41 | command: flake8 rest_framework_recaptcha 42 | 43 | complexity: 44 | <<: *dev-package 45 | command: xenon --max-absolute B --max-modules A --max-average A rest_framework_recaptcha 46 | 47 | test-unit: 48 | <<: *dev-package 49 | command: py.test -s tests/unit -vv --cov . --cov-config .coveragerc --cov-report term-missing --cov-report xml:reports/coverage.xml --cov-report html:reports/coverage.html 50 | volumes: 51 | - ./reports:/usr/src/app/reports 52 | 53 | test-all: 54 | <<: *dev-package 55 | command: tox 56 | 57 | security-sast: 58 | <<: *dev-package 59 | command: bandit -r rest_framework_recaptcha/. 60 | 61 | security-dependency-scan: 62 | <<: *dev-package 63 | command: safety check 64 | 65 | build-docs: 66 | <<: *dev-package-volumes 67 | command: /bin/bash -c "sphinx-apidoc -o docs/ rest_framework_recaptcha && make -C docs clean && make -C docs html" 68 | 69 | bumpversion-package: 70 | <<: *dev-package-volumes 71 | command: [ "bumpversion", "${VERSION_PART-patch}" ] 72 | 73 | build-package: 74 | <<: *package-volumes 75 | command: python setup.py sdist bdist_wheel 76 | 77 | publish-test-package: 78 | <<: *dev-package-volumes 79 | command: twine upload --repository-url https://test.pypi.org/legacy/ dist/* 80 | 81 | publish-package: 82 | <<: *dev-package-volumes 83 | command: twine upload dist/* 84 | 85 | generate-translations: 86 | <<: *package-volumes 87 | command: /bin/bash -c "cd rest_framework_recaptcha && django-admin makemessages --locale=${LOCALE}" 88 | 89 | update-translations: 90 | <<: *package-volumes 91 | command: /bin/bash -c "cd rest_framework_recaptcha && django-admin makemessages --all" 92 | 93 | compile-translations: 94 | <<: *package-volumes 95 | command: /bin/bash -c "cd rest_framework_recaptcha && django-admin compilemessages" 96 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # complexity documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 9 22:26:36 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | import rest_framework_recaptcha 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # sys.path.insert(0, os.path.abspath(".")) 23 | 24 | cwd = os.getcwd() 25 | parent = os.path.dirname(cwd) 26 | sys.path.append(parent) 27 | 28 | 29 | # -- General configuration ----------------------------------------------------- 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # needs_sphinx = "1.0" 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be extensions 35 | # coming with Sphinx (named "sphinx.ext.*") or your custom ones. 36 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ["_templates"] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = ".rst" 43 | 44 | # The encoding of source files. 45 | # source_encoding = "utf-8-sig" 46 | 47 | # The master toctree document. 48 | master_doc = "index" 49 | 50 | # General information about the project. 51 | project = "Django REST framework reCAPTCHA" 52 | copyright = "2018, Maximilien Raulic" 53 | author = "Maximilien Raulic" 54 | 55 | # The version info for the project you"re documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = rest_framework_recaptcha.__version__ 61 | # The full version, including alpha/beta/rc tags. 62 | release = rest_framework_recaptcha.__version__ 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | # today = "" 71 | # Else, today_fmt is used as the format for a strftime call. 72 | # today_fmt = "%B %d, %Y" 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ["_build"] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all documents. 79 | # default_role = None 80 | 81 | # If true, "()" will be appended to :func: etc. cross-reference text. 82 | # add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | # add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | # show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = "sphinx" 94 | 95 | # If true, `todo` and `todoList` produce output, else they produce nothing. 96 | todo_include_todos = False 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | # modindex_common_prefix = [] 100 | 101 | # If true, keep warnings as "system message" paragraphs in the built documents. 102 | # keep_warnings = False 103 | 104 | 105 | # -- Options for HTML output --------------------------------------------------- 106 | 107 | # The theme to use for HTML and HTML Help pages. See the documentation for 108 | # a list of builtin themes. 109 | html_theme = "default" 110 | 111 | # Theme options are theme-specific and customize the look and feel of a theme 112 | # further. For a list of options available for each theme, see the 113 | # documentation. 114 | # html_theme_options = {} 115 | 116 | # Add any paths that contain custom themes here, relative to this directory. 117 | # html_theme_path = [] 118 | 119 | # The name for this set of Sphinx documents. If None, it defaults to 120 | # " v documentation". 121 | # html_title = None 122 | 123 | # A shorter title for the navigation bar. Default is the same as html_title. 124 | # html_short_title = None 125 | 126 | # The name of an image file (relative to this directory) to place at the top 127 | # of the sidebar. 128 | # html_logo = None 129 | 130 | # The name of an image file (within the static path) to use as favicon of the 131 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 132 | # pixels large. 133 | # html_favicon = None 134 | 135 | # Add any paths that contain custom static files (such as style sheets) here, 136 | # relative to this directory. They are copied after the builtin static files, 137 | # so a file named "default.css" will overwrite the builtin "default.css". 138 | # html_static_path = ["_static"] 139 | 140 | # If not "", a "Last updated on:" timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | # html_last_updated_fmt = "%b %d, %Y" 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | # html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | # html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | # html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | # html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | # html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | # html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | # html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | # html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | # html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | # html_use_opensearch = "" 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | # html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = "djangorestframework-recaptchadoc" 183 | 184 | 185 | # -- Options for LaTeX output -------------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ("letterpaper" or "a4paper"). 189 | # "papersize": "letterpaper", 190 | # The font size ("10pt", "11pt" or "12pt"). 191 | # "pointsize": "10pt", 192 | # Additional stuff for the LaTeX preamble. 193 | # "preamble": "", 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, author, documentclass [howto/manual]). 198 | latex_documents = [ 199 | ( 200 | master_doc, 201 | "djangorestframework-recaptcha.tex", 202 | "Django REST framework reCAPTCHA Documentation", 203 | author, 204 | "manual", 205 | ) 206 | ] 207 | 208 | # The name of an image file (relative to this directory) to place at the top of 209 | # the title page. 210 | # latex_logo = None 211 | 212 | # For "manual" documents, if this is true, then toplevel headings are parts, 213 | # not chapters. 214 | # latex_use_parts = False 215 | 216 | # If true, show page references after internal links. 217 | # latex_show_pagerefs = False 218 | 219 | # If true, show URL addresses after external links. 220 | # latex_show_urls = False 221 | 222 | # Documents to append as an appendix to all manuals. 223 | # latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | # latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output -------------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [ 234 | ( 235 | master_doc, 236 | "djangorestframework-recaptcha", 237 | "Django REST framework reCAPTCHA Documentation", 238 | [author], 239 | 1, 240 | ) 241 | ] 242 | 243 | # If true, show URL addresses after external links. 244 | # man_show_urls = False 245 | 246 | 247 | # -- Options for Texinfo output ------------------------------------------------ 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ( 254 | master_doc, 255 | "djangorestframework-recaptcha", 256 | "Django REST framework reCAPTCHA Documentation", 257 | author, 258 | "djangorestframework-recaptcha", 259 | "reCAPTCHA field for Django REST framework serializers.", 260 | "Miscellaneous", 261 | ) 262 | ] 263 | 264 | # Documents to append as an appendix to all manuals. 265 | # texinfo_appendices = [] 266 | 267 | # If false, no module index is generated. 268 | # texinfo_domain_indices = True 269 | 270 | # How to display URL addresses: "footnote", "no", or "inline". 271 | # texinfo_show_urls = "footnote" 272 | 273 | # If true, do not generate a @detailmenu in the "Top" node"s menu. 274 | # texinfo_no_detailmenu = False 275 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------- 2 | Welcome to Django REST framework reCAPTCHA's documentation 3 | ---------------------------------------------------------- 4 | 5 | Django REST framework reCAPTCHA provides you a serializer field to handle and 6 | validate Google reCAPTCHA response. 7 | 8 | See our :doc:`Changelog ` for information on updates. 9 | 10 | Requirements 11 | ============ 12 | 13 | * Python: 2.7, 3.4, 3.5, 3.6, 3.7 14 | * Django: 1.10, 1.11, 2.0, 2.1 15 | * Django REST framework: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 16 | 17 | Sources 18 | ======= 19 | 20 | ``djangorestframework-recaptcha`` is hosted on 21 | `Github `_. 22 | 23 | Index 24 | ===== 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | installation 30 | usage 31 | contributing 32 | authors 33 | history 34 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | To install Django REST framework reCAPTCHA, run this command in your terminal: 5 | 6 | .. code-block:: console 7 | 8 | $ pip install djangorestframework-recaptcha 9 | 10 | This is the preferred method to install Django REST framework reCAPTCHA, as it 11 | will always install the most recent stable release. 12 | 13 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 14 | you through the process. 15 | 16 | .. _pip: https://pip.pypa.io 17 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 18 | 19 | Once the ``djangorestframework-recaptcha`` installed, add it to your 20 | ``INSTALLED_APPS``: 21 | 22 | .. code-block:: python 23 | 24 | INSTALLED_APPS = ( 25 | ... 26 | "rest_framework_recaptcha", 27 | ... 28 | ) 29 | 30 | Next, register yourself and obtain your reCAPTCHA credentials at 31 | https://www.google.com/recaptcha/admin. 32 | 33 | Finally, copy/paste your Google reCAPTCHA secret key to the 34 | ``DRF_RECAPTCHA_SECRET_KEY`` setting: 35 | 36 | .. code-block:: python 37 | 38 | DRF_RECAPTCHA_SECRET_KEY = "" 39 | 40 | Settings 41 | ======== 42 | 43 | ``DRF_RECAPTCHA_VERIFY_ENDPOINT`` 44 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | 46 | API endpoint to which to send the information to validate the reCAPTCHA 47 | response token. 48 | The default value is `https://www.google.com/recaptcha/api/siteverify` 49 | (cf. `reCAPTCHA documentation `_). 50 | 51 | ``DRF_RECAPTCHA_SECRET_KEY`` 52 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | Secret key of `your reCAPTCHA application `_. 55 | Don't forget to fill in this settings with your reCAPTCHA application secret 56 | key. 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | To use Django REST framework reCAPTCHA within your project you'll need to 5 | import and add the ``ReCaptchaField`` serializer field into the wanted 6 | serializer. For example: 7 | 8 | .. code-block:: python 9 | 10 | from rest_framework import serializers 11 | from rest_framework_recaptcha import ReCaptchaField 12 | 13 | 14 | class MySerializer(serializers.Serializer): 15 | recaptcha = ReCaptchaField() 16 | 17 | For you information, ``ReCaptchaField`` fields are defined as 18 | ``write_only=True`` by default. 19 | 20 | Once your serializer is configured with your ``ReCaptchaField`` you'll be able to 21 | send the client side generated reCAPTCHA response token and validate it 22 | server side (cf. `reCAPTCHA documentation `_). 23 | 24 | Validation errors 25 | ================= 26 | 27 | During the validation process of the reCAPTCHA response token by the 28 | verification API and according to the `documentation`_, the following errors 29 | may be raised: 30 | 31 | .. _documentation: https://developers.google.com/recaptcha/docs/verify#error-code-reference 32 | 33 | ====================== ============================================================== 34 | Error code Message 35 | ====================== ============================================================== 36 | missing-input-secret The secret parameter is missing. 37 | invalid-input-secret The secret parameter is invalid or malformed. 38 | missing-input-response The response parameter is missing. 39 | invalid-input-response The response parameter is invalid or malformed. 40 | bad-request The request is invalid or malformed. 41 | timeout-or-duplicate The response parameter has timed out or has already been used. 42 | ====================== ============================================================== 43 | 44 | Each of these errors are handled by the ``ReCaptchaValidator``. In case of an 45 | unknown error the ``bad-request`` error will be raised. 46 | 47 | Each error message can be replaced if needed. For this, you have two options: 48 | 49 | 1. Create a custom ``ReCaptchaField`` that inherits from ``ReCaptchaField`` 50 | while redefining the ``default_error_messages`` attribute with a dictionary 51 | which for each entry the key will match the code to override and the value 52 | to the new message. Example: 53 | 54 | .. code-block:: python 55 | 56 | from rest_framework import serializers 57 | from rest_framework_recaptcha import ReCaptchaField 58 | 59 | 60 | class MyReCaptchaField(ReCaptchaField): 61 | default_error_messages = { 62 | "invalid-input-response": "reCAPTCHA token is invalid.", 63 | } 64 | 65 | 66 | class MySerializer(serializers.Serializer): 67 | recaptcha = MyReCaptchaField() 68 | 69 | 2. When adding the ``ReCaptchaField`` field to your serializer you can pass the 70 | optional ``error_messages`` parameter with a dictionary which for each entry 71 | the key will match the code to override and the value to the new message. 72 | Example: 73 | 74 | .. code-block:: python 75 | 76 | from rest_framework import serializers 77 | from rest_framework_recaptcha import ReCaptchaField 78 | 79 | 80 | class MySerializer(serializers.Serializer): 81 | recaptcha = ReCaptchaField( 82 | error_messages={ 83 | "invalid-input-response": "reCAPTCHA token is invalid.", 84 | } 85 | ) 86 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.unit.settings 3 | -------------------------------------------------------------------------------- /reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/reports/.gitkeep -------------------------------------------------------------------------------- /rest_framework_recaptcha/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urllib.parse import urlencode # noqa: F401 3 | from urllib.request import urlopen # noqa: F401 4 | except ImportError: 5 | from urllib import urlencode # noqa: F401 6 | from urllib2 import urlopen # noqa: F401 7 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/conf.py: -------------------------------------------------------------------------------- 1 | from appconf import AppConf 2 | 3 | from django.conf import settings # noqa: F401 4 | 5 | 6 | class DjangoRestFrameworkRecaptchaConf(AppConf): 7 | """ 8 | Django REST framework reCAPTCHA settings. 9 | """ 10 | 11 | class Meta: 12 | prefix = "drf_recaptcha" 13 | 14 | VERIFY_ENDPOINT = "https://www.google.com/recaptcha/api/siteverify" 15 | 16 | SECRET_KEY = None 17 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/fields.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from rest_framework_recaptcha.validators import ReCaptchaValidator 4 | 5 | 6 | class ReCaptchaField(serializers.CharField): 7 | """ 8 | reCAPTCHA serializer field to use within a Django REST framework serializer 9 | in order to validate a reCAPTCHA response token. 10 | """ 11 | 12 | def __init__(self, write_only=True, **kwargs): 13 | """ 14 | Initializes the reCAPTCHA field as write only by default and append 15 | the ReCaptchaValidator to its validators list. 16 | :param write_only: determines whether or not the field is write only 17 | """ 18 | super(ReCaptchaField, self).__init__(write_only=write_only, **kwargs) 19 | self.validators.append( 20 | ReCaptchaValidator(messages=self.error_messages) 21 | ) 22 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/rest_framework_recaptcha/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /rest_framework_recaptcha/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-07-01 14:55+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: validators.py:15 22 | msgid "The request is invalid or malformed." 23 | msgstr "The request is invalid or malformed." 24 | 25 | #: validators.py:17 26 | msgid "The response parameter is invalid or malformed." 27 | msgstr "The response parameter is invalid or malformed." 28 | 29 | #: validators.py:19 30 | msgid "The secret parameter is invalid or malformed." 31 | msgstr "The secret parameter is invalid or malformed." 32 | 33 | #: validators.py:20 34 | msgid "The response parameter is missing." 35 | msgstr "The response parameter is missing." 36 | 37 | #: validators.py:21 38 | msgid "The secret parameter is missing." 39 | msgstr "The secret parameter is missing." 40 | 41 | #: validators.py:23 42 | msgid "The response parameter has timed out or has already been used." 43 | msgstr "The response parameter has timed out or has already been used." 44 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/rest_framework_recaptcha/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /rest_framework_recaptcha/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-07-01 14:55+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: validators.py:15 22 | msgid "The request is invalid or malformed." 23 | msgstr "La requête est invalide ou mal formée." 24 | 25 | #: validators.py:17 26 | msgid "The response parameter is invalid or malformed." 27 | msgstr "Le paramètre de réponse est invalide ou mal formé." 28 | 29 | #: validators.py:19 30 | msgid "The secret parameter is invalid or malformed." 31 | msgstr "" 32 | 33 | #: validators.py:20 34 | msgid "The response parameter is missing." 35 | msgstr "Le paramètre secret est invalide ou mal formé." 36 | 37 | #: validators.py:21 38 | msgid "The secret parameter is missing." 39 | msgstr "Le paramètre secret est manquant." 40 | 41 | #: validators.py:23 42 | msgid "The response parameter has timed out or has already been used." 43 | msgstr "Le paramètre de réponse a expiré ou a déjà été utilisé." 44 | -------------------------------------------------------------------------------- /rest_framework_recaptcha/validators.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ipware import get_client_ip 4 | from rest_framework import serializers 5 | 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from rest_framework_recaptcha.compat import urlencode, urlopen 10 | from rest_framework_recaptcha.conf import settings 11 | 12 | _DEFAULT_ERROR_CODE = "bad-request" 13 | 14 | _ERROR_MESSAGES = { 15 | _DEFAULT_ERROR_CODE: _("The request is invalid or malformed."), 16 | "invalid-input-response": _( 17 | "The response parameter is invalid or malformed." 18 | ), 19 | "invalid-input-secret": _("The secret parameter is invalid or malformed."), 20 | "missing-input-response": _("The response parameter is missing."), 21 | "missing-input-secret": _("The secret parameter is missing."), 22 | "timeout-or-duplicate": _( 23 | "The response parameter has timed out or has already been used." 24 | ), 25 | } 26 | 27 | 28 | class ReCaptchaValidator(object): 29 | """ 30 | A validator which check the reCAPTCHA response token. 31 | """ 32 | 33 | def __init__(self, messages=None): 34 | """ 35 | Initializes the validator with verify API endpoint and reCAPTCHA 36 | application's secret key. 37 | """ 38 | self._api_url = getattr( 39 | settings, "DRF_RECAPTCHA_VERIFY_ENDPOINT", None 40 | ) 41 | self._secret_key = getattr(settings, "DRF_RECAPTCHA_SECRET_KEY", None) 42 | self._client_ip = None 43 | 44 | self._error_messages = _ERROR_MESSAGES.copy() 45 | self._error_messages.update(messages or {}) 46 | 47 | def __call__(self, value): 48 | """ 49 | Performs the validation on the reCAPTCHA response token. 50 | :param value: reCAPTCHA response token 51 | :return: string 52 | """ 53 | if not (self._api_url and self._secret_key): 54 | raise ImproperlyConfigured( 55 | "`DRF_RECAPTCHA_VERIFY_ENDPOINT` and " 56 | "`DRF_RECAPTCHA_SECRET_KEY` should be both defined." 57 | ) 58 | 59 | response = self._get_recaptcha_response(value) 60 | if not response.get("success", False): 61 | error_codes = response.get("error-codes", []) 62 | if error_codes and error_codes[0] in self._error_messages: 63 | raise serializers.ValidationError( 64 | self._error_messages[error_codes[0]] 65 | ) 66 | raise serializers.ValidationError( 67 | self._error_messages[_DEFAULT_ERROR_CODE] 68 | ) 69 | return value 70 | 71 | def set_context(self, serializer_field): 72 | """ 73 | Try to determine the client ip address. 74 | :param serializer_field: reCAPTCHA field instance 75 | """ 76 | try: 77 | self._client_ip, _ = get_client_ip( 78 | serializer_field.context.get("request") 79 | ) 80 | except AttributeError: 81 | pass 82 | 83 | def _get_recaptcha_response(self, value): 84 | """ 85 | Calls the verify API endpoint and return its response. 86 | :param value: reCAPTCHA response token 87 | :return: dict 88 | """ 89 | values = {"secret": self._secret_key, "response": value} 90 | if self._client_ip: 91 | values["remoteip"] = self._client_ip 92 | 93 | data = urlencode(values).encode("ascii") 94 | 95 | try: 96 | with urlopen(self._api_url, data) as handler: 97 | return json.loads(handler.read().decode("utf-8")) 98 | except Exception: 99 | raise serializers.ValidationError( 100 | self._error_messages[_DEFAULT_ERROR_CODE] 101 | ) 102 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | 6 | from setuptools import find_packages, setup 7 | 8 | _BASE_FILE = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | def _get_version(*file_paths): 12 | """ 13 | Returns the version of the package. 14 | :param file_paths: path to the file containing the version number 15 | :return: string 16 | """ 17 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 18 | version_file = open(filename).read() 19 | version_match = re.search( 20 | r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M 21 | ) 22 | if version_match: 23 | return version_match.group(1) 24 | raise RuntimeError("Unable to find version string.") 25 | 26 | 27 | _VERSION = _get_version("rest_framework_recaptcha", "__init__.py") 28 | 29 | with open(os.path.join(_BASE_FILE, "README.rst")) as f: 30 | _README = f.read().strip() 31 | 32 | with open(os.path.join(_BASE_FILE, "HISTORY.rst")) as f: 33 | _HISTORY = f.read().replace(".. :changelog:", "").strip() 34 | 35 | 36 | _DESCRIPTION = """reCAPTCHA field for Django REST framework serializers.""" 37 | 38 | setup( 39 | name="djangorestframework-recaptcha", 40 | version=_VERSION, 41 | description=_DESCRIPTION, 42 | long_description=_README + "\n\n" + _HISTORY, 43 | long_description_content_type="text/x-rst", 44 | keywords="django rest framework recaptcha", 45 | author="Maximilien Raulic", 46 | author_email="maximilien.raulic@gmail.com", 47 | url="https://github.com/Maximilien-R/django-rest-framework-recaptcha", 48 | license="MIT", 49 | install_requires=[ 50 | "django>=1.10", 51 | "djangorestframework>=3", 52 | "django-appconf", 53 | "django-ipware>=2.1.0", 54 | "pytz", 55 | ], 56 | extras_require={ 57 | "development": [ 58 | "bandit==1.5.1", 59 | "black==18.9b0", 60 | "bumpversion==0.5.3", 61 | "flake8==3.6.0", 62 | "isort==4.3.4", 63 | "pytest-cov==2.6.1", 64 | "pytest-django==3.4.7", 65 | "pytest==4.3.0", 66 | "safety==1.8.5", 67 | "Sphinx==1.8.4", 68 | "tox==3.7.0", 69 | "twine==1.13.0", 70 | "xenon==0.5.5", 71 | ], 72 | }, 73 | packages=find_packages(include=["rest_framework_recaptcha"]), 74 | include_package_data=True, 75 | zip_safe=False, 76 | classifiers=[ 77 | "Development Status :: 3 - Alpha", 78 | "Framework :: Django :: 1.10", 79 | "Framework :: Django :: 1.11", 80 | "Framework :: Django :: 2.0", 81 | "Framework :: Django :: 2.1", 82 | "Intended Audience :: Developers", 83 | "License :: OSI Approved :: BSD License", 84 | "Natural Language :: English", 85 | "Programming Language :: Python :: 2", 86 | "Programming Language :: Python :: 2.7", 87 | "Programming Language :: Python :: 3", 88 | "Programming Language :: Python :: 3.4", 89 | "Programming Language :: Python :: 3.5", 90 | "Programming Language :: Python :: 3.6", 91 | "Programming Language :: Python :: 3.7", 92 | ], 93 | ) 94 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/drf_recaptcha_field/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maximilien-R/django-rest-framework-recaptcha/3cf6628583321ccdeb5b65b59145413e2265296e/tests/unit/drf_recaptcha_field/__init__.py -------------------------------------------------------------------------------- /tests/unit/drf_recaptcha_field/test_fields.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rest_framework import serializers 4 | 5 | from rest_framework_recaptcha.fields import ReCaptchaField 6 | from rest_framework_recaptcha.validators import ReCaptchaValidator 7 | 8 | try: 9 | from unittest import mock 10 | except ImportError: 11 | import mock 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("params", "expected"), 16 | [ 17 | ({}, True), 18 | ( 19 | {"write_only": False}, 20 | False, 21 | ), 22 | ( 23 | {"write_only": True}, 24 | True, 25 | ), 26 | ], 27 | ) 28 | def test_recaptchafield_write_only(params, expected): 29 | field = ReCaptchaField(**params) 30 | assert field.write_only is expected 31 | 32 | 33 | def test_recaptchafield_has_recaptcha_validator(): 34 | field = ReCaptchaField() 35 | 36 | nb_validators = len(field.validators) 37 | assert nb_validators > 0 38 | assert isinstance(field.validators[nb_validators - 1], ReCaptchaValidator) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | ("messages", "recaptcha_response", "expected_error"), 43 | [ 44 | ( 45 | {"bad-request": "bad-request"}, 46 | {"success": False, "error-codes": ["bad-request"]}, 47 | "bad-request", 48 | ), 49 | ( 50 | {"invalid-input-response": "invalid-input-response"}, 51 | {"success": False, "error-codes": ["invalid-input-response"]}, 52 | "invalid-input-response", 53 | ), 54 | ( 55 | {"invalid-input-secret": "invalid-input-secret"}, 56 | {"success": False, "error-codes": ["invalid-input-secret"]}, 57 | "invalid-input-secret", 58 | ), 59 | ( 60 | {"missing-input-response": "missing-input-response"}, 61 | {"success": False, "error-codes": ["missing-input-response"]}, 62 | "missing-input-response", 63 | ), 64 | ( 65 | {"missing-input-secret": "missing-input-secret"}, 66 | {"success": False, "error-codes": ["missing-input-secret"]}, 67 | "missing-input-secret", 68 | ), 69 | ( 70 | {"timeout-or-duplicate": "timeout-or-duplicate"}, 71 | {"success": False, "error-codes": ["timeout-or-duplicate"]}, 72 | "timeout-or-duplicate", 73 | ), 74 | ] 75 | ) 76 | def test_recaptchafield_validation_default_error_messages_error( 77 | messages, recaptcha_response, expected_error 78 | ): 79 | class CustomReCaptchaField(ReCaptchaField): 80 | default_error_messages = messages 81 | 82 | field = CustomReCaptchaField() 83 | 84 | nb_validators = len(field.validators) 85 | assert nb_validators > 0 86 | assert isinstance(field.validators[nb_validators - 1], ReCaptchaValidator) 87 | 88 | field.validators[nb_validators - 1]._get_recaptcha_response = mock.Mock( 89 | return_value=recaptcha_response 90 | ) 91 | 92 | with pytest.raises(serializers.ValidationError) as excinfo: 93 | field.run_validators("token") 94 | 95 | assert expected_error in str(excinfo.value) 96 | 97 | 98 | @pytest.mark.parametrize( 99 | ("messages", "recaptcha_response", "expected_error"), 100 | [ 101 | ( 102 | {"bad-request": "bad-request"}, 103 | {"success": False, "error-codes": ["bad-request"]}, 104 | "bad-request", 105 | ), 106 | ( 107 | {"invalid-input-response": "invalid-input-response"}, 108 | {"success": False, "error-codes": ["invalid-input-response"]}, 109 | "invalid-input-response", 110 | ), 111 | ( 112 | {"invalid-input-secret": "invalid-input-secret"}, 113 | {"success": False, "error-codes": ["invalid-input-secret"]}, 114 | "invalid-input-secret", 115 | ), 116 | ( 117 | {"missing-input-response": "missing-input-response"}, 118 | {"success": False, "error-codes": ["missing-input-response"]}, 119 | "missing-input-response", 120 | ), 121 | ( 122 | {"missing-input-secret": "missing-input-secret"}, 123 | {"success": False, "error-codes": ["missing-input-secret"]}, 124 | "missing-input-secret", 125 | ), 126 | ( 127 | {"timeout-or-duplicate": "timeout-or-duplicate"}, 128 | {"success": False, "error-codes": ["timeout-or-duplicate"]}, 129 | "timeout-or-duplicate", 130 | ), 131 | ] 132 | ) 133 | def test_recaptchafield_validation_messages_error( 134 | messages, recaptcha_response, expected_error 135 | ): 136 | field = ReCaptchaField(error_messages=messages) 137 | 138 | nb_validators = len(field.validators) 139 | assert nb_validators > 0 140 | assert isinstance(field.validators[nb_validators - 1], ReCaptchaValidator) 141 | 142 | field.validators[nb_validators - 1]._get_recaptcha_response = mock.Mock( 143 | return_value=recaptcha_response 144 | ) 145 | 146 | with pytest.raises(serializers.ValidationError) as excinfo: 147 | field.run_validators("token") 148 | 149 | assert expected_error in str(excinfo.value) 150 | -------------------------------------------------------------------------------- /tests/unit/drf_recaptcha_field/test_validators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rest_framework import serializers 4 | 5 | from django.conf import settings 6 | from django.core.exceptions import ImproperlyConfigured 7 | 8 | from rest_framework_recaptcha import validators 9 | 10 | try: 11 | from unittest import mock 12 | except ImportError: 13 | import mock 14 | 15 | 16 | @pytest.mark.parametrize( 17 | ("secret_key", "api_url"), 18 | [ 19 | (None, None), 20 | (None, "DRF_RECAPTCHA_VERIFY_ENDPOINT"), 21 | ("DRF_RECAPTCHA_SECRET_KEY", None), 22 | ], 23 | ) 24 | def test_recaptchavalidator_call_improperly_configured( 25 | monkeypatch, api_url, secret_key 26 | ): 27 | monkeypatch.setattr(settings, "DRF_RECAPTCHA_SECRET_KEY", secret_key) 28 | monkeypatch.setattr( 29 | settings, "DRF_RECAPTCHA_VERIFY_ENDPOINT", api_url 30 | ) 31 | 32 | validator = validators.ReCaptchaValidator() 33 | 34 | with pytest.raises(ImproperlyConfigured) as excinfo: 35 | validator("token") 36 | 37 | assert str(excinfo.value) == ( 38 | "`DRF_RECAPTCHA_VERIFY_ENDPOINT` and " 39 | "`DRF_RECAPTCHA_SECRET_KEY` should be both defined." 40 | ) 41 | 42 | 43 | @pytest.mark.parametrize( 44 | ("messages", "recaptcha_response", "expected_error"), 45 | [ 46 | ( 47 | {}, 48 | {}, 49 | "The request is invalid or malformed." 50 | ), 51 | ( 52 | {}, 53 | {"success": False}, 54 | "The request is invalid or malformed." 55 | ), 56 | ( 57 | {}, 58 | {"success": False, "error-codes": []}, 59 | "The request is invalid or malformed.", 60 | ), 61 | ( 62 | {}, 63 | {"success": False, "error-codes": ["unknown"]}, 64 | "The request is invalid or malformed.", 65 | ), 66 | ( 67 | {}, 68 | { 69 | "success": False, 70 | "error-codes": ["unknown", "invalid-input-response"], 71 | }, 72 | "The request is invalid or malformed.", 73 | ), 74 | ( 75 | {}, 76 | {"success": False, "error-codes": ["bad-request"]}, 77 | "The request is invalid or malformed.", 78 | ), 79 | ( 80 | {}, 81 | {"success": False, "error-codes": ["invalid-input-response"]}, 82 | "The response parameter is invalid or malformed.", 83 | ), 84 | ( 85 | {}, 86 | {"success": False, "error-codes": ["invalid-input-secret"]}, 87 | "The secret parameter is invalid or malformed.", 88 | ), 89 | ( 90 | {}, 91 | {"success": False, "error-codes": ["missing-input-response"]}, 92 | "The response parameter is missing.", 93 | ), 94 | ( 95 | {}, 96 | {"success": False, "error-codes": ["missing-input-secret"]}, 97 | "The secret parameter is missing.", 98 | ), 99 | ( 100 | {}, 101 | {"success": False, "error-codes": ["timeout-or-duplicate"]}, 102 | "The response parameter has timed out or has already been used.", 103 | ), 104 | ( 105 | {}, 106 | { 107 | "success": False, 108 | "error-codes": ["missing-input-secret", "bad-request"], 109 | }, 110 | "The secret parameter is missing.", 111 | ), 112 | ( 113 | {"bad-request": "bad-request"}, 114 | {"success": False, "error-codes": ["bad-request"]}, 115 | "bad-request", 116 | ), 117 | ( 118 | {"invalid-input-response": "invalid-input-response"}, 119 | {"success": False, "error-codes": ["invalid-input-response"]}, 120 | "invalid-input-response", 121 | ), 122 | ( 123 | {"invalid-input-secret": "invalid-input-secret"}, 124 | {"success": False, "error-codes": ["invalid-input-secret"]}, 125 | "invalid-input-secret", 126 | ), 127 | ( 128 | {"missing-input-response": "missing-input-response"}, 129 | {"success": False, "error-codes": ["missing-input-response"]}, 130 | "missing-input-response", 131 | ), 132 | ( 133 | {"missing-input-secret": "missing-input-secret"}, 134 | {"success": False, "error-codes": ["missing-input-secret"]}, 135 | "missing-input-secret", 136 | ), 137 | ( 138 | {"timeout-or-duplicate": "timeout-or-duplicate"}, 139 | {"success": False, "error-codes": ["timeout-or-duplicate"]}, 140 | "timeout-or-duplicate", 141 | ), 142 | ], 143 | ) 144 | def test_recaptchavalidator_call_validation_error( 145 | messages, recaptcha_response, expected_error 146 | ): 147 | validator = validators.ReCaptchaValidator(messages=messages) 148 | validator._get_recaptcha_response = mock.Mock( 149 | return_value=recaptcha_response 150 | ) 151 | 152 | with pytest.raises(serializers.ValidationError) as excinfo: 153 | validator("token") 154 | 155 | assert expected_error in str(excinfo.value) 156 | 157 | 158 | def test_recaptchavalidator_call__success(): 159 | validator = validators.ReCaptchaValidator() 160 | validator._get_recaptcha_response = mock.Mock( 161 | return_value={"success": True} 162 | ) 163 | assert validator("token") == "token" 164 | 165 | 166 | @pytest.mark.parametrize( 167 | "serializer_field", 168 | [ 169 | None, 170 | {}, 171 | mock.Mock(context={}), 172 | mock.Mock(context={"request": None}), 173 | mock.Mock(context={"request": mock.Mock(META=None)}), 174 | ], 175 | ) 176 | def test_recaptchavalidator_set_context_attributeerror(serializer_field): 177 | validator = validators.ReCaptchaValidator() 178 | assert validator._client_ip is None 179 | validator.set_context(serializer_field) 180 | assert validator._client_ip is None 181 | 182 | 183 | def test_recaptchavalidator_set_context(): 184 | validator = validators.ReCaptchaValidator() 185 | assert validator._client_ip is None 186 | serializer_field = mock.Mock( 187 | context={ 188 | "request": mock.Mock(META={"HTTP_X_FORWARDED_FOR": "172.10.20.3"}) 189 | } 190 | ) 191 | validator.set_context(serializer_field) 192 | assert validator._client_ip == "172.10.20.3" 193 | 194 | 195 | @pytest.mark.parametrize("field", [ 196 | mock.Mock(context={"request": mock.Mock(META={})}), 197 | mock.Mock( 198 | context={ 199 | "request": mock.Mock(META={"HTTP_X_FORWARDED_FOR": "172.10.20.3"}) 200 | } 201 | ), 202 | ]) 203 | def test_recaptchavalidator_get_recaptcha_response(field): 204 | validator = validators.ReCaptchaValidator() 205 | validator.set_context(field) 206 | 207 | cm = mock.MagicMock() 208 | cm.decode.return_value = '{"success": true}' 209 | cm.read.return_value = cm 210 | cm.__enter__.return_value = cm 211 | 212 | with mock.patch.object( 213 | validators, "urlencode", wraps=validators.urlencode 214 | ) as urlencode_mock: 215 | with mock.patch.object(validators, "urlopen") as urlopen_mock: 216 | urlopen_mock.return_value = cm 217 | read_mock = urlopen_mock.return_value.read 218 | 219 | assert validator._get_recaptcha_response("token") == { 220 | "success": True 221 | } 222 | 223 | assert read_mock.called 224 | assert urlopen_mock.call_count == 1 225 | 226 | assert read_mock.called 227 | assert read_mock.call_count == 1 228 | 229 | read_mock.return_value.decode.assert_called_once_with("utf-8") 230 | 231 | urlencode_data = { 232 | "secret": "DRF_RECAPTCHA_SECRET_KEY", 233 | "response": "token", 234 | } 235 | if field.context["request"].META: 236 | urlencode_data["remoteip"] = ( 237 | field.context["request"].META["HTTP_X_FORWARDED_FOR"] 238 | ) 239 | urlencode_mock.assert_called_once_with(urlencode_data) 240 | 241 | 242 | @pytest.mark.parametrize("field", [ 243 | mock.Mock(context={"request": mock.Mock(META={})}), 244 | mock.Mock( 245 | context={ 246 | "request": mock.Mock(META={"HTTP_X_FORWARDED_FOR": "172.10.20.3"}) 247 | } 248 | ), 249 | ]) 250 | def test_recaptchavalidator_get_recaptcha_response_throw_exception(field): 251 | validator = validators.ReCaptchaValidator() 252 | validator.set_context(field) 253 | 254 | with mock.patch.object( 255 | validators, "urlencode", return_value="encoded" 256 | ) as urlencode_mock: 257 | with mock.patch.object( 258 | validators, "urlopen", side_effect=[Exception()] 259 | ) as urlopen_mock: 260 | with pytest.raises(serializers.ValidationError) as excinfo: 261 | validator._get_recaptcha_response("token") 262 | urlopen_mock.assert_called_once_with( 263 | "DRF_RECAPTCHA_VERIFY_ENDPOINT", b"encoded" 264 | ) 265 | 266 | urlencode_data = { 267 | "secret": "DRF_RECAPTCHA_SECRET_KEY", 268 | "response": "token", 269 | } 270 | if field.context["request"].META: 271 | urlencode_data["remoteip"] = ( 272 | field.context["request"].META["HTTP_X_FORWARDED_FOR"] 273 | ) 274 | urlencode_mock.assert_called_once_with(urlencode_data) 275 | assert "The request is invalid or malformed" in str(excinfo.value) 276 | -------------------------------------------------------------------------------- /tests/unit/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import django 5 | 6 | DEBUG = True 7 | 8 | USE_TZ = True 9 | 10 | SECRET_KEY = "1ws*(=vz7@)albx#ltw9#azm$&y4-w73_mof+n39+9dhtlsdi7" 11 | 12 | DATABASES = { 13 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} 14 | } 15 | 16 | ROOT_URLCONF = "tests.urls" 17 | 18 | INSTALLED_APPS = [] 19 | 20 | SITE_ID = 1 21 | 22 | if django.VERSION >= (1, 10): 23 | MIDDLEWARE = () 24 | else: 25 | MIDDLEWARE_CLASSES = () 26 | 27 | DRF_RECAPTCHA_VERIFY_ENDPOINT = "DRF_RECAPTCHA_VERIFY_ENDPOINT" 28 | 29 | DRF_RECAPTCHA_SECRET_KEY = "DRF_RECAPTCHA_SECRET_KEY" 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | style 4 | complexity 5 | security 6 | drf{34,35,36}-py{27,34,35}-django110 7 | drf{37,38,39}-py{27,34,35,36,37}-django{110,111} 8 | drf{37,38,39}-py{34,35,36,37}-django20 9 | drf{37,38,39}-py{35,36,37}-django21 10 | 11 | [testenv] 12 | setenv = 13 | DJANGO_SETTINGS_MODULE = tests.unit.settings 14 | PYTHONPATH = {toxinidir} 15 | PYTHONWARNINGS = all 16 | PYTHONDONTWRITEBYTECODE = 1 17 | passenv = 18 | TRAVIS 19 | TRAVIS_* 20 | deps = 21 | django110: Django>=1.10,<1.11 22 | django111: Django>=1.11,<1.12 23 | django20: Django>=2.0,<2.1 24 | django21: Django>=2.1,<2.2 25 | drf34: djangorestframework>=3.4,<3.5 26 | drf35: djangorestframework>=3.5,<3.6 27 | drf36: djangorestframework>=3.6,<3.7 28 | drf37: djangorestframework>=3.7,<3.8 29 | drf38: djangorestframework>=3.8,<3.9 30 | drf39: djangorestframework>=3.9,<3.10 31 | py27: mock 32 | coveralls 33 | pytest 34 | pytest-cov 35 | pytest-django 36 | pytest-xdist 37 | commands = 38 | py.test --cov rest_framework_recaptcha --cov-config .coveragerc --cov-report= {posargs} 39 | coveralls 40 | 41 | [testenv:style] 42 | basepython = python3.6 43 | extras = development 44 | commands = 45 | isort -rc rest_framework_recaptcha/. 46 | black -l 79 --py36 --check rest_framework_recaptcha 47 | flake8 rest_framework_recaptcha 48 | 49 | [testenv:complexity] 50 | basepython = python3.6 51 | extras = development 52 | commands = 53 | xenon --max-absolute B --max-modules A --max-average A rest_framework_recaptcha 54 | 55 | [testenv:security] 56 | basepython = python3.6 57 | extras = development 58 | commands = 59 | bandit -r rest_framework_recaptcha/. 60 | safety check 61 | --------------------------------------------------------------------------------