From b15736accd21d7debd3448aaaedede642d7b8654 Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Thu, 3 Jan 2013 19:07:00 -0800 Subject: [PATCH 1/7] RegisterFormMixin can now to_dict all fields. It adds a to_dict function that uses inspect to add all wtf Field to the returned dict. This allows extensions to the register form to easily add fields that will be passed to the datastore's create_user function. --- flask_security/forms.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/flask_security/forms.py b/flask_security/forms.py index 373debc..77cc065 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -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 @@ -89,6 +91,10 @@ class NextFormMixin(): class RegisterFormMixin(): submit = SubmitField("Register") + def to_dict(form): + fields = inspect.getmembers(form, lambda member: isinstance(member, Field)) + return dict((key, value.data) for key, value in fields) + class SendConfirmationForm(Form, UserEmailFormMixin): """The default forgot password form""" @@ -172,10 +178,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 From f83092865bce130404de25bdec9e5c94c08d2e84 Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Thu, 3 Jan 2013 22:00:29 -0800 Subject: [PATCH 2/7] Configurable forms, issue:49 --- docs/customizing.rst | 33 +++++++++++++++++++++++++++++- flask_security/core.py | 25 +++++++++++++++++++++++ flask_security/views.py | 45 +++++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/docs/customizing.rst b/docs/customizing.rst index d10a522..3aff81c 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -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") \ No newline at end of file + return dict(hello="world") diff --git a/flask_security/core.py b/flask_security/core.py index 94d60c8..aee6093 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -167,6 +167,7 @@ def _get_state(app, datastore, **kwargs): reset_serializer=_get_serializer(app, 'reset'), confirm_serializer=_get_serializer(app, 'confirm'), _context_processors={}, + _form_fns={}, _send_mail_task=None )) @@ -236,6 +237,9 @@ class _SecurityState(object): rv.update(fn()) return rv + def _get_form_cls(self, endpoint): + return self._form_fns.get(endpoint, lambda: None)() + def context_processor(self, fn): self._add_ctx_processor(None, fn) @@ -263,6 +267,27 @@ class _SecurityState(object): def send_mail_task(self, fn): self._send_mail_task = fn + def login_form(self, fn): + self._form_fns['login_form'] = fn + + def confirm_register_form(self, fn): + self._form_fns['confirm_register_form'] = fn + + def register_form(self, fn): + self._form_fns['register_form'] = fn + + def forgot_password_form(self, fn): + self._form_fns['forgot_password_form'] = fn + + def reset_password_form(self, fn): + self._form_fns['reset_password_form'] = fn + + def send_confirmation_form(self, fn): + self._form_fns['send_confirmation_form'] = fn + + def passwordless_login_form(self, fn): + self._form_fns['passwordless_login_form'] = fn + class Security(object): """The :class:`Security` class initializes the Flask-Security extension. diff --git a/flask_security/views.py b/flask_security/views.py index c079ea7..72a493a 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -35,6 +35,17 @@ _security = LocalProxy(lambda: current_app.extensions['security']) _datastore = LocalProxy(lambda: _security.datastore) +_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 _render_json(form, include_auth_token=False): has_errors = len(form.errors) > 0 @@ -60,14 +71,20 @@ def _ctx(endpoint): return _security._run_ctx_processor(endpoint) +def _form_cls(endpoint): + return _security._get_form_cls(endpoint) or _default_forms[endpoint] + + @anonymous_user_required def login(): """View function for login view""" + form_class = _form_cls('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 +118,9 @@ def register(): """View function which handles a registration request.""" if _security.confirmable or request.json: - form_class = ConfirmRegisterForm + form_class = _form_cls('confirm_register_form') else: - form_class = RegisterForm + form_class = _form_cls('register_form') if request.json: form_data = MultiDict(request.json) @@ -136,10 +153,12 @@ def register(): def send_login(): """View function that sends login instructions for passwordless login""" + form_class = _form_cls('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 +198,12 @@ def token_login(token): def send_confirmation(): """View function which sends confirmation instructions.""" + form_class = _form_cls('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 +246,12 @@ def confirm_email(token): def forgot_password(): """View function that handles a forgotten password request.""" + form_class = _form_cls('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 +280,7 @@ def reset_password(token): if invalid or expired: return redirect(url_for('forgot_password')) - form = ResetPasswordForm() + form = _form_cls('reset_password_form')() if form.validate_on_submit(): after_this_request(_commit) From 1a87a4cd0c2e0ff31ef7d318b5e96ccdb758b29a Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Thu, 3 Jan 2013 23:29:50 -0800 Subject: [PATCH 3/7] Fix to RegisterForm.to_dict. Only add fields that are also attributes on the datastorage.user_model. --- flask_security/forms.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask_security/forms.py b/flask_security/forms.py index 77cc065..182167a 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -92,7 +92,10 @@ class RegisterFormMixin(): submit = SubmitField("Register") def to_dict(form): - fields = inspect.getmembers(form, lambda member: isinstance(member, Field)) + 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) From 09fe5a2cb752555cb24e794bde0710365a97435e Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Sun, 6 Jan 2013 20:00:13 -0800 Subject: [PATCH 4/7] Make test_app take kwargs --- tests/__init__.py | 10 +++++++--- tests/configured_tests.py | 22 +++++++++++++++++++--- tests/functional_tests.py | 8 ++++---- tests/test_app/mongoengine.py | 4 ++-- tests/test_app/sqlalchemy.py | 8 ++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 6d1f451..53e0d9b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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, diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 45f0fc6..0c6b11d 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -468,13 +468,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 +484,18 @@ 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): + + APP_KWARGS = { + } + + AUTH_CONFIG = { + 'SECURITY_REGISTERABLE': True, + } + + def test_register(self): + r = self._get('/register') + self.assertIn('

Register

', r.data) + diff --git a/tests/functional_tests.py b/tests/functional_tests.py index c9fd8da..49e9be6 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -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) diff --git a/tests/test_app/mongoengine.py b/tests/test_app/mongoengine.py index 52377f2..d40f666 100644 --- a/tests/test_app/mongoengine.py +++ b/tests/test_app/mongoengine.py @@ -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) diff --git a/tests/test_app/sqlalchemy.py b/tests/test_app/sqlalchemy.py index 0cf2e9c..ec23b01 100644 --- a/tests/test_app/sqlalchemy.py +++ b/tests/test_app/sqlalchemy.py @@ -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) From 81040a57a6735b6eab8a3450f345b8d6f0e9a6b8 Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Sun, 6 Jan 2013 20:20:06 -0800 Subject: [PATCH 5/7] Views get forms from _security --- flask_security/core.py | 44 ++++++++++++++++----------------------- flask_security/views.py | 32 +++++++--------------------- tests/configured_tests.py | 12 +++++++++++ 3 files changed, 37 insertions(+), 51 deletions(-) diff --git a/flask_security/core.py b/flask_security/core.py index aee6093..a29a573 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -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']) @@ -93,6 +96,17 @@ _default_messages = { } +_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) @@ -167,10 +181,11 @@ def _get_state(app, datastore, **kwargs): reset_serializer=_get_serializer(app, 'reset'), confirm_serializer=_get_serializer(app, 'confirm'), _context_processors={}, - _form_fns={}, _send_mail_task=None )) + kwargs.update(_default_forms) + return _SecurityState(**kwargs) @@ -237,9 +252,6 @@ class _SecurityState(object): rv.update(fn()) return rv - def _get_form_cls(self, endpoint): - return self._form_fns.get(endpoint, lambda: None)() - def context_processor(self, fn): self._add_ctx_processor(None, fn) @@ -267,27 +279,6 @@ class _SecurityState(object): def send_mail_task(self, fn): self._send_mail_task = fn - def login_form(self, fn): - self._form_fns['login_form'] = fn - - def confirm_register_form(self, fn): - self._form_fns['confirm_register_form'] = fn - - def register_form(self, fn): - self._form_fns['register_form'] = fn - - def forgot_password_form(self, fn): - self._form_fns['forgot_password_form'] = fn - - def reset_password_form(self, fn): - self._form_fns['reset_password_form'] = fn - - def send_confirmation_form(self, fn): - self._form_fns['send_confirmation_form'] = fn - - def passwordless_login_form(self, fn): - self._form_fns['passwordless_login_form'] = fn - class Security(object): """The :class:`Security` class initializes the Flask-Security extension. @@ -302,12 +293,13 @@ 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, **kwargs): """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 diff --git a/flask_security/views.py b/flask_security/views.py index 72a493a..3efd316 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -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, \ @@ -35,17 +32,6 @@ _security = LocalProxy(lambda: current_app.extensions['security']) _datastore = LocalProxy(lambda: _security.datastore) -_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 _render_json(form, include_auth_token=False): has_errors = len(form.errors) > 0 @@ -71,15 +57,11 @@ def _ctx(endpoint): return _security._run_ctx_processor(endpoint) -def _form_cls(endpoint): - return _security._get_form_cls(endpoint) or _default_forms[endpoint] - - @anonymous_user_required def login(): """View function for login view""" - form_class = _form_cls('login_form') + form_class = _security.login_form if request.json: form = form_class(MultiDict(request.json)) @@ -118,9 +100,9 @@ def register(): """View function which handles a registration request.""" if _security.confirmable or request.json: - form_class = _form_cls('confirm_register_form') + form_class = _security.confirm_register_form else: - form_class = _form_cls('register_form') + form_class = _security.register_form if request.json: form_data = MultiDict(request.json) @@ -153,7 +135,7 @@ def register(): def send_login(): """View function that sends login instructions for passwordless login""" - form_class = _form_cls('passwordless_login_form') + form_class = _security.passwordless_login_form if request.json: form = form_class(MultiDict(request.json)) @@ -198,7 +180,7 @@ def token_login(token): def send_confirmation(): """View function which sends confirmation instructions.""" - form_class = _form_cls('send_confirmation_form') + form_class = _security.send_confirmation_form if request.json: form = form_class(MultiDict(request.json)) @@ -246,7 +228,7 @@ def confirm_email(token): def forgot_password(): """View function that handles a forgotten password request.""" - form_class = _form_cls('forgot_password_form') + form_class = _security.forgot_password_form if request.json: form = form_class(MultiDict(request.json)) @@ -280,7 +262,7 @@ def reset_password(token): if invalid or expired: return redirect(url_for('forgot_password')) - form = _form_cls('reset_password_form')() + form = _security.reset_password_form() if form.validate_on_submit(): after_this_request(_commit) diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 0c6b11d..92a5d3f 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -6,6 +6,10 @@ 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 tests import SecurityTest @@ -488,13 +492,21 @@ class NoBlueprintTests(SecurityTest): class ExtendFormsTest(SecurityTest): + class MyLoginForm(LoginForm): + hidden_tag = 'not-so-secret' + APP_KWARGS = { + 'login_form': MyLoginForm, } AUTH_CONFIG = { 'SECURITY_REGISTERABLE': True, } + def test_login_view(self): + r = self._get('/login') + self.assertIn("

Login

", r.data) + def test_register(self): r = self._get('/register') self.assertIn('

Register

', r.data) From ca0d1d0b506c5b8643e07bb8fc630362ffbfb718 Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Mon, 7 Jan 2013 21:43:27 -0800 Subject: [PATCH 6/7] All unit-tests for configurable forms --- tests/configured_tests.py | 94 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 92a5d3f..f1e0d7d 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -9,6 +9,7 @@ from flask.ext.security.utils import capture_registrations, \ 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 @@ -493,21 +494,104 @@ class NoBlueprintTests(SecurityTest): class ExtendFormsTest(SecurityTest): class MyLoginForm(LoginForm): - hidden_tag = 'not-so-secret' + 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') - self.assertIn("

Login

", r.data) + r = self._get('/login', follow_redirects=True) + self.assertIn("My Login Email Address Field", r.data) def test_register(self): - r = self._get('/register') - self.assertIn('

Register

', r.data) + 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) From e4190a031553f149f081383fcdfe7a57df66794c Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Mon, 7 Jan 2013 21:43:33 -0800 Subject: [PATCH 7/7] Add kwargs for configurable forms. Specifically list out the kwargs so we'll get an interpreter error on a bad name. --- flask_security/core.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/flask_security/core.py b/flask_security/core.py index a29a573..db92707 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -184,7 +184,9 @@ def _get_state(app, datastore, **kwargs): _send_mail_task=None )) - kwargs.update(_default_forms) + for key, value in _default_forms.items(): + if key not in kwargs or not kwargs[key]: + kwargs[key] = value return _SecurityState(**kwargs) @@ -293,7 +295,11 @@ 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, **kwargs): + 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. @@ -311,7 +317,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__))