From 9a47ec1ed966d8c67dfd5b504b131ae161573958 Mon Sep 17 00:00:00 2001 From: Eskil Heyn Olsen Date: Fri, 11 Jan 2013 22:35:54 -0800 Subject: [PATCH] Working on change password form --- flask_security/core.py | 11 +++++-- flask_security/forms.py | 25 +++++++++++++--- flask_security/templates/security/_menu.html | 5 +++- flask_security/views.py | 31 ++++++++++++++++++++ tests/configured_tests.py | 27 +++++++++++++++-- 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/flask_security/core.py b/flask_security/core.py index b413c74..6ae5ba4 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -97,7 +97,8 @@ _default_messages = { 'DISABLED_ACCOUNT': ('Account is disabled.', 'error'), 'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'), 'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'), - 'PASSWORD_CHANGE': ('You successfully changes your password.', 'success'), + 'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'), + 'INVALID_PASSWORD': ('Invalid password', 'error'), 'LOGIN': ('Please log in to access this page.', 'info'), 'REFRESH': ('Please reauthenticate to access this page.', 'info') } @@ -294,6 +295,9 @@ class _SecurityState(object): def reset_password_context_processor(self, fn): self._add_ctx_processor('reset_password', fn) + def change_password_context_processor(self, fn): + self._add_ctx_processor('change_password', fn) + def send_confirmation_context_processor(self, fn): self._add_ctx_processor('send_confirmation', fn) @@ -323,8 +327,8 @@ class Security(object): 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): + reset_password_form=None, change_password_form=None, + send_confirmation_form=None, passwordless_login_form=None): """Initializes the Flask-Security extension for the specified application and datastore implentation. @@ -348,6 +352,7 @@ class Security(object): register_form=register_form, forgot_password_form=forgot_password_form, reset_password_form=reset_password_form, + change_password_form=change_password_form, send_confirmation_form=send_confirmation_form, passwordless_login_form=passwordless_login_form) diff --git a/flask_security/forms.py b/flask_security/forms.py index 9234322..4b2808b 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -15,10 +15,11 @@ from flask import request, current_app from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \ SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \ ValidationError, Length, Field +from flask.ext.login import current_user from werkzeug.local import LocalProxy from .confirmable import requires_confirmation -from .utils import verify_and_update_password, get_message +from .utils import verify_password, verify_and_update_password, get_message # Convenient reference _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) @@ -83,6 +84,7 @@ class NewPasswordFormMixin(): validators=[password_required, Length(min=6, max=128)]) + class PasswordConfirmFormMixin(): password_confirm = PasswordField("Retype Password", validators=[EqualTo('password', message="Passwords do not match")]) @@ -97,7 +99,8 @@ class RegisterFormMixin(): def to_dict(form): def is_field_and_user_attr(member): - return isinstance(member, Field) and hasattr(_datastore.user_model, member.name) + 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) @@ -172,7 +175,7 @@ class LoginForm(Form, NextFormMixin): self.email.errors.append('Specified user does not exist') return False if not verify_and_update_password(self.password.data, self.user): - self.password.errors.append('Invalid password') + self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False if requires_confirmation(self.user): self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) @@ -197,11 +200,25 @@ class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): submit = SubmitField("Reset Password") -class ChangePasswordForm(Form, PasswordFormMixin, PasswordConfirmFormMixin): +class ChangePasswordForm(Form, PasswordFormMixin): """The default Change password form""" new_password = PasswordField("New Password", validators=[password_required, Length(min=6, max=128)]) + new_password_confirm = PasswordField("Retype Password", + validators=[EqualTo('new_password', message="Passwords do not match")]) + submit = SubmitField("Change Password") + + def validate(self): + super(ChangePasswordForm, self).validate() + + if self.password.data.strip() == '': + self.password.errors.append('Password not provided') + return False + if not verify_and_update_password(self.password.data, current_user): + self.password.errors.append(get_message('INVALID_PASSWORD')[0]) + return False + return True diff --git a/flask_security/templates/security/_menu.html b/flask_security/templates/security/_menu.html index d50b5a4..272cd22 100644 --- a/flask_security/templates/security/_menu.html +++ b/flask_security/templates/security/_menu.html @@ -5,6 +5,9 @@ {% if security.registerable %}
  • Register
  • {% endif %} + {% if security.changeable %} +
  • Change password
  • + {% endif %} {% if security.recoverable %}
  • Forgot password
  • {% endif %} @@ -12,4 +15,4 @@
  • Confirm account
  • {% endif %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/flask_security/views.py b/flask_security/views.py index 3efd316..6a810f5 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -278,6 +278,32 @@ def reset_password(token): **_ctx('reset_password')) +@login_required +def change_password(): + """View function which handles a change password request.""" + + form_class = _security.change_password_form + + if request.json: + form = form_class(MultiDict(request.json)) + else: + form = form_class() + + if form.validate_on_submit(): + #send_reset_password_instructions(form.user) + print 'VALID' + if request.json is None: + #do_flash(*get_message('PASSWORD_CHANGE')) + do_flash('dicks') + + if request.json: + return _render_json(form) + + return render_template('security/change_password.html', + change_password_form=form, + **_ctx('change_password')) + + def create_blueprint(state, import_name): """Creates the security extension blueprint""" @@ -311,6 +337,11 @@ def create_blueprint(state, import_name): methods=['GET', 'POST'], endpoint='reset_password')(reset_password) + if state.changeable: + bp.route(state.change_url, + methods=['GET', 'POST'], + endpoint='change_password')(change_password) + if state.confirmable: bp.route(state.confirm_url, methods=['GET', 'POST'], diff --git a/tests/configured_tests.py b/tests/configured_tests.py index fc00353..9d874b9 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -327,12 +327,35 @@ class ChangePasswordTest(SecurityTest): def test_change_password(self): self.authenticate() - r = self.client.post('/change/' + t, data={ + r = self.client.get('/change', follow_redirects=True) + self.assertIn('Change password', r.data) + + def test_change_password_invalid(self): + self.authenticate() + r = self.client.post('/change', data={ + 'password': 'notpassword', + 'new_password': 'newpassword', + 'new_password_confirm': 'newpassword' + }, follow_redirects=True) + print r + self.assertIn('Invalid password', r.data) + + def test_change_password_mismatch(self): + self.authenticate() + r = self.client.post('/change', data={ + 'password': 'password', + 'new_password': 'newpassword', + 'new_password_confirm': 'notnewpassword' + }, follow_redirects=True) + self.assertIn('Passwords do not match', r.data) + + def test_change_password_success(self): + self.authenticate() + r = self.client.post('/change', data={ 'password': 'password', 'new_password': 'newpassword', 'new_password_confirm': 'newpassword' }, follow_redirects=True) - self.assertIn('You successfully changed your password', r.data)