Skip to content

Async validators + Recaptcha #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
831039e
async validate_on_submit prototype
omarryhan Dec 24, 2018
6428966
Improved tests structure
omarryhan Dec 24, 2018
cf202cf
More async validators tests
omarryhan Dec 24, 2018
fb9c9b5
Added support for async field.post and pre validators
omarryhan Dec 24, 2018
47cce97
Added Travis CI
omarryhan Dec 24, 2018
7438ce5
Fix travis 3.7
omarryhan Dec 24, 2018
98f3ed2
Fix travis 3.7
omarryhan Dec 24, 2018
fead664
Sanic-wtf will now fail if a user patches wtforms and continues using…
omarryhan Dec 24, 2018
0b8e672
Sanic-wtf will now fail if a user patches wtforms and continues using…
omarryhan Dec 24, 2018
f615ce3
Pytest now warns if any
omarryhan Dec 24, 2018
32b0504
added doctstings
omarryhan Dec 24, 2018
cf5e7e6
Added support for Recaptcha
omarryhan Dec 26, 2018
ebed361
Documented Recaptcha integration
omarryhan Dec 26, 2018
81f066e
100% recaptcha test cov
omarryhan Dec 26, 2018
5d60b72
Recaptcha docstrings
omarryhan Dec 26, 2018
cd44950
Minor docstring fix
omarryhan Dec 26, 2018
1ca89dc
Fixed typo
omarryhan Dec 26, 2018
6f2d1c4
Fixed typo
omarryhan Dec 26, 2018
d8d41ee
Moved config_prefix into kwargs
omarryhan Dec 26, 2018
5030040
added test for config prefix
omarryhan Dec 26, 2018
3689f31
Fixed more typos
omarryhan Dec 26, 2018
796b53e
Removed unused patching code
omarryhan Dec 27, 2018
3579e29
Added option for JS only
omarryhan Dec 27, 2018
1797632
Validators now validate in order. Cov @94%
omarryhan Dec 27, 2018
bdcc7b0
Replaced recaptcha's validators param with extra_validaors
omarryhan Feb 28, 2019
a92cf50
recaptcha field passing list instead of set to super()
omarryhan Feb 28, 2019
51b0782
Better recaptcha json/form error handling
omarryhan Feb 28, 2019
cbc1e24
Recaptcha only using aiorecaptcha's error msg not the full error object
omarryhan Feb 28, 2019
7078a2d
Fixed test
omarryhan Feb 28, 2019
238f1ad
More efficient recaptcha validator
omarryhan Feb 28, 2019
41cda4f
Minor optimization tweak
omarryhan Mar 21, 2019
804d94b
Updated README
omarryhan Apr 12, 2019
76d143f
Removed commented code
omarryhan Apr 12, 2019
53d3e28
Drop Python3.5 dependancy
omarryhan Jun 27, 2019
e62ff7c
Hardcoded wtforms version to avoid breakage of patching logic on upda…
omarryhan Jun 27, 2019
c4df028
Bumped version
omarryhan Jun 27, 2019
714e7f6
Remove python 3.5 testing from .travis yaml
omarryhan Jun 27, 2019
41c24f0
More descriptive code comments
omarryhan Jun 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
sudo: false
language: python
python:
- 3.6
# <Workaround>. Python 3.7 not yet supported on Travis
matrix:
include:
- python: 3.7
dist: xenial
sudo: true
# </Workaround>
install:
pip install tox-travis
script:
tox
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a new line to the end

24 changes: 18 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Sanic-WTF - Sanic meets WTForms
===============================

Sanic-WTF makes using `WTForms` with `Sanic`_ and CSRF (Cross-Site Request
Forgery) protection a little bit easier.
Forgery) protection a little bit easier now with Recaptcha and async validators.


.. _WTForms: https://github.com/wtforms/wtforms
Expand All @@ -19,7 +19,7 @@ Installation

.. code-block:: sh

pip install --upgrade Sanic-WTF
pip install Sanic-WTF


How to use it
Expand All @@ -37,6 +37,8 @@ Intialization (of Sanic)

# either WTF_CSRF_SECRET_KEY or SECRET_KEY should be set
app.config['WTF_CSRF_SECRET_KEY'] = 'top secret!'
app.config['RECAPTCHA_PUBLIC_KEY'] = 'Public key'
app.config['RECAPTCHA_PRIVATE_KEY'] = 'Private key'

@app.middleware('request')
async def add_session_to_request(request):
Expand All @@ -48,14 +50,23 @@ Defining Forms

.. code-block:: python

from sanic_wtf import SanicForm
from sanic_wtf import SanicForm, RecaptchaField
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(SanicForm):
name = StringField('Name', validators=[DataRequired()])
async def email_not_exist_validator(form, field):
user_id = await get_user_id_by_email(
conn=form.request.app.db,
email=form.signup_email.data
)
if user_id:
raise ValidationError('A user with the given email already exists')

class SignupForm(SanicForm):
email = StringField('Email', validators=[DataRequired(), email_not_exist_validator])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Sign In')
recaptcha = RecaptchaField('recaptcha')

That's it, just subclass `SanicForm` and later on passing in the current
`request` object when you instantiate the form class. Sanic-WTF will do the
Expand All @@ -72,7 +83,8 @@ Form Validation
@app.route('/', methods=['GET', 'POST'])
async def index(request):
form = LoginForm(request)
if request.method == 'POST' and form.validate():
if request.method == 'POST' and \
await form.validate_on_submit_async():
name = form.name.data
password = form.password.data
# check user password, log in user, etc.
Expand Down
Empty file added clear
Empty file.
3 changes: 3 additions & 0 deletions clear_pycache.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf
133 changes: 133 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ to try `Sanic-CookieSession`_.
Configuration
=============

CSRF Configs
---------------

================================ =============================================
Option Description
================================ =============================================
Expand All @@ -31,6 +34,136 @@ Option Description
Default is `1800`. (Half an hour)
================================ =============================================

Recaptcha Configs
------------------

RECAPTCHA_PUBLIC_KEY:

* Required

* Your Sitekey

RECAPTCHA_PRIVATE_KEY:

* Required

RECAPTCHA_ONLOAD (str):

* Optional

* The name of your callback function to be executed once all the dependencies have loaded.

RECAPTCHA_RENDER (str):

* Optional

* Whether to render the widget explicitly.

* Defaults to onload, which will render the widget in the first g-recaptcha tag it finds.

* Either: ``"onload"`` or explicitly specify a widget value

RECAPTCHA_LANGUAGE (str):

* Optional

* hl language code

* Reference: https://developers.google.com/recaptcha/docs/language

RECAPTCHA_ASYNC (bool):

* Optional

* add async tag to JS script

* Default True

RECAPTCHA_DEFER (bool):

* Optional

* Add def tag to JS Script

* Default True

RECAPTCHA_THEME:

* The color theme of the widget.

* Optional

* One of: (dark, light)

* Default: light

RECAPTCHA_BADGE:

* Reposition the reCAPTCHA badge. 'inline' lets you position it with CSS.

* Optional

* One of: ('bottomright', 'bottomleft', 'inline')

* Default: None

RECAPTCHA_SIZE:

* Optional

* The size of the widget

* One of: ("compact", "normal", "invisible")

* Default: 'normal'

RECAPTCHA_TYPE:

* Optional

* One of: ('image', 'audio')

* Default: 'image'

RECAPTCHA_TABINDEX (int):

* Optional

* The tabindex of the widget and challenge.

* If other elements in your page use tabindex, it should be set to make user navigation easier.

* Default: 0

RECAPTCHA_CALLBACK (str):

* Optional

* The name of your callback function, executed when the user submits a successful response.

* The **g-recaptcha-response** token is passed to your callback.

RECAPTCHA_EXPIRED_CALLBACK (str):

* Opional

* The name of your callback function, executed when the reCAPTCHA response expires and the user needs to re-verify.

RECAPTCHA_ERROR_CALLBACK (str):

* Optional

* The name of your callback function, executed when reCAPTCHA encounters an error
(usually network connectivity) and cannot continue until connectivity is restored.

* If you specify a function here, you are responsible for informing the user that they should retry.

RECAPTCHA_JS_ONLY (bool):

* Default False

* You might need this if you only want to use Recaptcha's JS script (Recaptcha V3)


API
===
Expand Down
46 changes: 41 additions & 5 deletions sanic_wtf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from wtforms.csrf.session import SessionCSRF
from wtforms.meta import DefaultMeta
from wtforms.validators import DataRequired, StopValidation
from wtforms.fields.core import Field
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field isn't used


__version__ = '0.6.0.dev0'
from ._patch import patch
from .recaptcha import RecaptchaField
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to use absolute imports


__version__ = '1.0.3.dev0'

__all__ = [
'SanicForm',
'FileAllowed', 'file_allowed', 'FileRequired', 'file_required',
'FileAllowed', 'file_allowed', 'FileRequired', 'file_required', 'RecaptchaField'
]


Expand Down Expand Up @@ -110,7 +114,6 @@ def getlist(self, name, default=None):
"""
return super().get(name, default)


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to pep8 here should be 2 blank lines, no need to remove 1 blank line
https://www.python.org/dev/peps/pep-0008/#blank-lines

class SanicForm(Form):
"""Form with session-based CSRF Protection.

Expand All @@ -123,10 +126,15 @@ class Meta(DefaultMeta):
csrf_class = SessionCSRF

def __init__(self, request=None, *args, meta=None, **kwargs):
# Patching status
self.patched = False

# Meta
form_meta = meta_for_request(request)
form_meta.update(meta or {})
kwargs['meta'] = form_meta

# Formdata
self.request = request
if request is not None:
formdata = kwargs.pop('formdata', sentinel)
Expand All @@ -141,7 +149,35 @@ def __init__(self, request=None, *args, meta=None, **kwargs):

super().__init__(*args, **kwargs)

# Pass app to fields that need it
if self.request is not None:
for name, field in self._fields.items():
if hasattr(field, '_get_app'):
field._get_app(self.request.app)

# @unpatch ??
def validate_on_submit(self):
''' For async validators: use self.validate_on_submit_async.
This method is only still here for backward compatibility
'''
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.patched is not False:
raise RuntimeError('Once you go async, you can never go back. :)\
Continue using validate_on_submit_async \
instead of validate_on submit')
"""Return `True` if this form is submited and all fields verified"""
request = self.request
return request and request.method in SUBMIT_VERBS and self.validate()
return self.request and (self.request.method in SUBMIT_VERBS) and \
self.validate()

@patch
async def validate_on_submit_async(self):
''' Adds support for async validators and Sanic-WTF Recaptcha

.. note::

As a side effect of patching wtforms to support async,
there's a restriction you must be aware of:
Don't use SanifForm.validate_on_submit() (the sync version) after running this method.
Doing so will most likely cause an error.
'''
return self.request and (self.request.method in SUBMIT_VERBS) and \
await self.validate()
Loading