From 330ee324ba33f53e2ce9df55183f1a287b19896e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 14 Aug 2024 15:55:10 -0500 Subject: [PATCH] feat: expose User password for editing in master views --- src/wuttaweb/forms/base.py | 7 +++++++ src/wuttaweb/forms/widgets.py | 3 +++ src/wuttaweb/views/auth.py | 10 +++++----- src/wuttaweb/views/users.py | 18 ++++++++++++++++-- tests/forms/test_base.py | 12 ++++++++++++ tests/views/test_users.py | 13 +++++++++++++ 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index c667468..be76882 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -674,6 +674,13 @@ class Form: schema = SQLAlchemySchemaNode(self.model_class, includes=includes) + # fill in the blanks if anything got missed + for key in fields: + if key not in schema: + log.warning("key '%s' not in schema", key) + node = colander.SchemaNode(colander.String(), name=key) + schema.add(node) + else: # make basic schema diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index b766af0..b4d8254 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -31,12 +31,15 @@ in the namespace: * :class:`deform:deform.widget.Widget` (base class) * :class:`deform:deform.widget.TextInputWidget` * :class:`deform:deform.widget.TextAreaWidget` +* :class:`deform:deform.widget.PasswordWidget` +* :class:`deform:deform.widget.CheckedPasswordWidget` * :class:`deform:deform.widget.SelectWidget` * :class:`deform:deform.widget.CheckboxChoiceWidget` """ import colander from deform.widget import (Widget, TextInputWidget, TextAreaWidget, + PasswordWidget, CheckedPasswordWidget, SelectWidget, CheckboxChoiceWidget) from webhelpers2.html import HTML diff --git a/src/wuttaweb/views/auth.py b/src/wuttaweb/views/auth.py index 894752a..5acd697 100644 --- a/src/wuttaweb/views/auth.py +++ b/src/wuttaweb/views/auth.py @@ -25,11 +25,11 @@ Auth Views """ import colander -from deform.widget import TextInputWidget, PasswordWidget, CheckedPasswordWidget from wuttaweb.views import View from wuttaweb.db import Session from wuttaweb.auth import login_user, logout_user +from wuttaweb.forms import widgets class AuthView(View): @@ -97,14 +97,14 @@ class AuthView(View): schema.add(colander.SchemaNode( colander.String(), name='username', - widget=TextInputWidget(attributes={ + widget=widgets.TextInputWidget(attributes={ 'ref': 'username', }))) schema.add(colander.SchemaNode( colander.String(), name='password', - widget=PasswordWidget(attributes={ + widget=widgets.PasswordWidget(attributes={ 'ref': 'password', }))) @@ -174,13 +174,13 @@ class AuthView(View): schema.add(colander.SchemaNode( colander.String(), name='current_password', - widget=PasswordWidget(), + widget=widgets.PasswordWidget(), validator=self.change_password_validate_current_password)) schema.add(colander.SchemaNode( colander.String(), name='new_password', - widget=CheckedPasswordWidget(), + widget=widgets.CheckedPasswordWidget(), validator=self.change_password_validate_new_password)) return schema diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py index 0c017c6..4f4b6f0 100644 --- a/src/wuttaweb/views/users.py +++ b/src/wuttaweb/views/users.py @@ -28,6 +28,7 @@ import colander from wuttjamaican.db.model import User from wuttaweb.views import MasterView +from wuttaweb.forms import widgets from wuttaweb.forms.schema import PersonRef, RoleRefs from wuttaweb.db import Session @@ -93,9 +94,15 @@ class UserView(MasterView): f.set_validator('username', self.unique_username) # password - # if not (self.creating or self.editing): - # f.remove('password') + # nb. we must avoid 'password' as field name since + # ColanderAlchemy wants to handle the raw/hashed value f.remove('password') + # nb. no need for password field if readonly + if self.creating or self.editing: + # nb. use 'set_password' as field name + f.append('set_password') + f.set_required('set_password', False) + f.set_widget('set_password', widgets.CheckedPasswordWidget()) # roles f.append('roles') @@ -121,9 +128,16 @@ class UserView(MasterView): def objectify(self, form, session=None): """ """ + data = form.validated + # normal logic first user = super().objectify(form) + # maybe set user password + if 'set_password' in form and data.get('set_password'): + auth = self.app.get_auth_handler() + auth.set_user_password(user, data['set_password']) + # update roles for user # TODO # if self.has_perm('edit_roles'): diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py index 86b411a..f75f1b7 100644 --- a/tests/forms/test_base.py +++ b/tests/forms/test_base.py @@ -218,6 +218,18 @@ class TestForm(TestCase): self.assertIn('name', schema) self.assertIn('value', schema) + # ColanderAlchemy schema still has *all* requested fields + form = self.make_form(model_instance=model.Setting(name='uhoh'), + fields=['name', 'value', 'foo', 'bar']) + self.assertEqual(form.fields, ['name', 'value', 'foo', 'bar']) + self.assertIsNone(form.schema) + schema = form.get_schema() + self.assertEqual(len(schema.children), 4) + self.assertIn('name', schema) + self.assertIn('value', schema) + self.assertIn('foo', schema) + self.assertIn('bar', schema) + # schema nodes are required by default form = self.make_form(fields=['foo', 'bar']) schema = form.get_schema() diff --git a/tests/views/test_users.py b/tests/views/test_users.py index 292297b..54870ba 100644 --- a/tests/views/test_users.py +++ b/tests/views/test_users.py @@ -15,6 +15,9 @@ class TestUserView(WebTestCase): def make_view(self): return mod.UserView(self.request) + def test_includeme(self): + self.pyramid_config.include('wuttaweb.views.users') + def test_get_query(self): view = self.make_view() query = view.get_query(session=self.session) @@ -76,11 +79,13 @@ class TestUserView(WebTestCase): def test_objectify(self): model = self.app.model + auth = self.app.get_auth_handler() blokes = model.Role(name="Blokes") self.session.add(blokes) others = model.Role(name="Others") self.session.add(others) barney = model.User(username='barney') + auth.set_user_password(barney, 'testpass') barney.roles.append(blokes) self.session.add(barney) self.session.commit() @@ -93,6 +98,14 @@ class TestUserView(WebTestCase): self.assertEqual(len(barney.roles), 1) self.assertEqual(barney.roles[0].name, "Blokes") + # form can update user password + self.assertTrue(auth.check_user_password(barney, 'testpass')) + form = view.make_model_form(model_instance=barney) + form.validated = {'username': 'barney', 'set_password': 'testpass2'} + user = view.objectify(form, session=self.session) + self.assertIs(user, barney) + self.assertTrue(auth.check_user_password(barney, 'testpass2')) + # form can update user roles form = view.make_model_form(model_instance=barney) form.validated = {'username': 'barney', 'roles': {others.uuid}}