Merge pull request #58 from eskil/registerform

Configurable forms
This commit is contained in:
Matt Wright
2013-01-08 07:10:14 -08:00
9 changed files with 226 additions and 38 deletions
+32 -1
View File
@@ -59,6 +59,37 @@ The following is a list of all the available context processor decorators:
* ``send_login_context_processor``: Send login view
Forms
-----
All forms can be overridden in much the same way as context
processors. For each form used, you can specify a function that
returns the form class. This allows you to override/extend forms. For
example, to add extra fields to the register form::
security = Security(app, user_datastore)
from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()])
last_name = TextField('Last Name', [Required()])
@security.register_form
def security_register_form():
return ExtendedRegisterForm
The following is a list of all the available form override decorators:
* ``login_form``: Login form
* ``confirm_register_form``: Confirmable register form
* ``register_form``: Register form
* ``forgot_password_form``: Forgot password form
* ``reset_password_form``: Reset password form
* ``send_confirmation_form``: Send confirmation form
* ``passwordless_login_form``: Passwordless login form
Emails
------
@@ -93,4 +124,4 @@ templates you can specify an email context processor with the
# This processor is added to all emails
@security.email_context_processor
def security_mail_processor():
return dict(hello="world")
return dict(hello="world")
+32 -2
View File
@@ -21,6 +21,9 @@ from werkzeug.local import LocalProxy
from .utils import config_value as cv, get_config, md5, url_for_security
from .views import create_blueprint
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
@@ -105,6 +108,17 @@ _allowed_password_hash_schemes = [
'plaintext'
]
_default_forms = {
'login_form': LoginForm,
'confirm_register_form': ConfirmRegisterForm,
'register_form': RegisterForm,
'forgot_password_form': ForgotPasswordForm,
'reset_password_form': ResetPasswordForm,
'send_confirmation_form': SendConfirmationForm,
'passwordless_login_form': PasswordlessLoginForm,
}
def _user_loader(user_id):
return _security.datastore.find_user(id=user_id)
@@ -189,6 +203,10 @@ def _get_state(app, datastore, **kwargs):
_send_mail_task=None
))
for key, value in _default_forms.items():
if key not in kwargs or not kwargs[key]:
kwargs[key] = value
return _SecurityState(**kwargs)
@@ -296,12 +314,17 @@ class Security(object):
if app is not None and datastore is not None:
self._state = self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore=None, register_blueprint=True):
def init_app(self, app, datastore=None, register_blueprint=True,
login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None,
reset_password_form=None, send_confirmation_form=None,
passwordless_login_form=None):
"""Initializes the Flask-Security extension for the specified
application and datastore implentation.
:param app: The application.
:param datastore: An instance of a user datastore.
:param register_blueprint: to register the Security blueprint or not.
"""
datastore = datastore or self.datastore
@@ -313,7 +336,14 @@ class Security(object):
identity_loaded.connect_via(app)(_on_identity_loaded)
state = _get_state(app, datastore)
state = _get_state(app, datastore,
login_form=login_form,
confirm_register_form=confirm_register_form,
register_form=register_form,
forgot_password_form=forgot_password_form,
reset_password_form=reset_password_form,
send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form)
if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__))
+11 -5
View File
@@ -9,10 +9,12 @@
:license: MIT, see LICENSE for more details.
"""
import inspect
from flask import request, current_app
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \
ValidationError, Length
ValidationError, Length, Field
from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation
@@ -93,6 +95,13 @@ class NextFormMixin():
class RegisterFormMixin():
submit = SubmitField("Register")
def to_dict(form):
def is_field_and_user_attr(member):
return isinstance(member, Field) and hasattr(_datastore.user_model, member.name)
fields = inspect.getmembers(form, is_field_and_user_attr)
return dict((key, value.data) for key, value in fields)
class SendConfirmationForm(Form, UserEmailFormMixin):
"""The default forgot password form"""
@@ -176,10 +185,7 @@ class LoginForm(Form, NextFormMixin):
class ConfirmRegisterForm(Form, RegisterFormMixin,
UniqueEmailFormMixin, NewPasswordFormMixin):
def to_dict(self):
return dict(email=self.email.data,
password=self.password.data)
pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
pass
+19 -14
View File
@@ -17,9 +17,6 @@ from werkzeug.local import LocalProxy
from .confirmable import send_confirmation_instructions, \
confirm_user, confirm_email_token_status
from .decorators import login_required, anonymous_user_required
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from .passwordless import send_login_instructions, \
login_token_status
from .recoverable import reset_password_token_status, \
@@ -64,10 +61,12 @@ def _ctx(endpoint):
def login():
"""View function for login view"""
form_class = _security.login_form
if request.json:
form = LoginForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = LoginForm()
form = form_class()
if form.validate_on_submit():
login_user(form.user, remember=form.remember.data)
@@ -101,9 +100,9 @@ def register():
"""View function which handles a registration request."""
if _security.confirmable or request.json:
form_class = ConfirmRegisterForm
form_class = _security.confirm_register_form
else:
form_class = RegisterForm
form_class = _security.register_form
if request.json:
form_data = MultiDict(request.json)
@@ -136,10 +135,12 @@ def register():
def send_login():
"""View function that sends login instructions for passwordless login"""
form_class = _security.passwordless_login_form
if request.json:
form = PasswordlessLoginForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = PasswordlessLoginForm()
form = form_class()
if form.validate_on_submit():
send_login_instructions(form.user)
@@ -179,10 +180,12 @@ def token_login(token):
def send_confirmation():
"""View function which sends confirmation instructions."""
form_class = _security.send_confirmation_form
if request.json:
form = SendConfirmationForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = SendConfirmationForm()
form = form_class()
if form.validate_on_submit():
send_confirmation_instructions(form.user)
@@ -225,10 +228,12 @@ def confirm_email(token):
def forgot_password():
"""View function that handles a forgotten password request."""
form_class = _security.forgot_password_form
if request.json:
form = ForgotPasswordForm(MultiDict(request.json))
form = form_class(MultiDict(request.json))
else:
form = ForgotPasswordForm()
form = form_class()
if form.validate_on_submit():
send_reset_password_instructions(form.user)
@@ -257,7 +262,7 @@ def reset_password(token):
if invalid or expired:
return redirect(url_for('forgot_password'))
form = ResetPasswordForm()
form = _security.reset_password_form()
if form.validate_on_submit():
after_this_request(_commit)
+7 -3
View File
@@ -5,20 +5,24 @@ from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
APP_KWARGS = {
'register_blueprint': True,
}
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
app = self._create_app(self.AUTH_CONFIG or {})
app_kwargs = self.APP_KWARGS
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
app.debug = False
app.config['TESTING'] = True
self.app = app
self.client = app.test_client()
def _create_app(self, auth_config, register_blueprint=True):
return create_app(auth_config, register_blueprint)
def _create_app(self, auth_config, **kwargs):
return create_app(auth_config, **kwargs)
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
+115 -3
View File
@@ -6,6 +6,11 @@ import simplejson as json
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
from tests import SecurityTest
@@ -468,13 +473,14 @@ class AsyncMailTaskTests(SecurityTest):
class NoBlueprintTests(SecurityTest):
APP_KWARGS = {
'register_blueprint': False,
}
AUTH_CONFIG = {
'USER_COUNT': 1
}
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
@@ -483,3 +489,109 @@ class NoBlueprintTests(SecurityTest):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data)
class ExtendFormsTest(SecurityTest):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
APP_KWARGS = {
'login_form': MyLoginForm,
'register_form': MyRegisterForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': False,
'SECURITY_REGISTERABLE': True,
}
def test_login_view(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Login Email Address Field", r.data)
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Register Email Address Field", r.data)
class RecoverableExtendFormsTest(SecurityTest):
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Password Email Address Field',
validators=[valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
submit = SubmitField("My Reset Password Submit Field")
APP_KWARGS = {
'forgot_password_form': MyForgotPasswordForm,
'reset_password_form': MyResetPasswordForm,
}
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
}
def test_forgot_password(self):
r = self._get('/reset', follow_redirects=True)
self.assertIn("My Forgot Password Email Address Field", r.data)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn("My Reset Password Submit Field", r.data)
class PasswordlessExtendFormsTest(SecurityTest):
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Login Email Address Field')
APP_KWARGS = {
'passwordless_login_form': MyPasswordlessLoginForm,
}
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
}
def test_passwordless_login(self):
r = self._get('/login', follow_redirects=True)
self.assertIn("My Passwordless Login Email Address Field", r.data)
class ConfirmableExtendFormsTest(SecurityTest):
class MyConfirmRegisterForm(ConfirmRegisterForm):
email = TextField('My Confirm Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
APP_KWARGS = {
'confirm_register_form': MyConfirmRegisterForm,
'send_confirmation_form': MySendConfirmationForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn("My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True)
self.assertIn("My Send Confirmation Email Address Field", r.data)
+4 -4
View File
@@ -201,9 +201,9 @@ class DefaultSecurityTests(SecurityTest):
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest):
@@ -231,6 +231,6 @@ class DefaultDatastoreTests(SecurityTest):
class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
return create_app(auth_config, **kwargs)
+2 -2
View File
@@ -13,7 +13,7 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config):
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['MONGODB_SETTINGS'] = dict(
@@ -46,7 +46,7 @@ def create_app(config):
Role.drop_collection()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, MongoEngineUserDatastore(db, User, Role))
app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)
+4 -4
View File
@@ -14,10 +14,11 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, register_blueprint=True):
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
#app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app)
@@ -50,8 +51,7 @@ def create_app(config, register_blueprint=True):
db.create_all()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, SQLAlchemyUserDatastore(db, User, Role),
register_blueprint=register_blueprint)
app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)