From 5b2d1dad53199fc760b4dcaf09846fcc329127cf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 27 Jan 2025 17:07:42 -0600 Subject: [PATCH] feat: overhaul some User/Person form fields etc. hoping this is more intuitive to use.. --- src/wuttaweb/forms/schema.py | 22 ----- src/wuttaweb/forms/widgets.py | 58 ------------ src/wuttaweb/views/people.py | 67 +++++++++++--- src/wuttaweb/views/users.py | 68 ++++++++++---- tests/forms/test_schema.py | 14 --- tests/forms/test_widgets.py | 47 +--------- tests/views/test_people.py | 64 ++++++++++--- tests/views/test_users.py | 166 +++++++++++++++++++++++++++------- 8 files changed, 289 insertions(+), 217 deletions(-) diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 77e443e..605b2c5 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -572,28 +572,6 @@ class RoleRefs(WuttaSet): return widgets.RoleRefsWidget(self.request, **kwargs) -class UserRefs(WuttaSet): - """ - Form schema type for the Role - :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` - association proxy field. - - This is a subclass of :class:`WuttaSet`. It uses a ``set`` of - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` ``uuid`` - values for underlying data format. - """ - - def widget_maker(self, **kwargs): - """ - Constructs a default widget for the field. - - :returns: Instance of - :class:`~wuttaweb.forms.widgets.UserRefsWidget`. - """ - kwargs.setdefault('session', Session()) - return widgets.UserRefsWidget(self.request, **kwargs) - - class Permissions(WuttaSet): """ Form schema type for the Role diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index f87e8b1..03fe717 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -56,7 +56,6 @@ from webhelpers2.html import HTML from wuttjamaican.conf import parse_list from wuttaweb.db import Session -from wuttaweb.grids import Grid class ObjectRefWidget(SelectWidget): @@ -414,63 +413,6 @@ class RoleRefsWidget(WuttaCheckboxChoiceWidget): return super().serialize(field, cstruct, **kw) -class UserRefsWidget(WuttaCheckboxChoiceWidget): - """ - Widget for use with Role - :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field. - This is the default widget for the - :class:`~wuttaweb.forms.schema.UserRefs` type. - - This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however - it only supports readonly mode and does not use a template. - Rather, it generates and renders a - :class:`~wuttaweb.grids.base.Grid` showing the users list. - """ - - def serialize(self, field, cstruct, **kw): - """ """ - readonly = kw.get('readonly', self.readonly) - if not readonly: - raise NotImplementedError("edit not allowed for this widget") - - model = self.app.model - columns = ['username', 'active'] - - # generate data set for users - users = [] - if cstruct: - for uuid in cstruct: - user = self.session.get(model.User, uuid) - if user: - users.append(dict([(key, getattr(user, key)) - for key in columns + ['uuid']])) - - # do not render if no data - if not users: - return HTML.tag('span') - - # grid - grid = Grid(self.request, key='roles.view.users', - columns=columns, data=users) - - # view action - if self.request.has_perm('users.view'): - url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid']) - grid.add_action('view', icon='eye', url=url) - grid.set_link('person') - grid.set_link('username') - - # edit action - if self.request.has_perm('users.edit'): - url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid']) - grid.add_action('edit', url=url) - - # render as simple - # nb. must indicate we are a part of this form - form = getattr(field.parent, 'wutta_form', None) - return grid.render_table_element(form) - - class PermissionsWidget(WuttaCheckboxChoiceWidget): """ Widget for use with Role diff --git a/src/wuttaweb/views/people.py b/src/wuttaweb/views/people.py index 78cf931..c8ce87c 100644 --- a/src/wuttaweb/views/people.py +++ b/src/wuttaweb/views/people.py @@ -28,7 +28,6 @@ import sqlalchemy as sa from wuttjamaican.db.model import Person from wuttaweb.views import MasterView -from wuttaweb.forms.schema import UserRefs class PersonView(MasterView): @@ -62,6 +61,14 @@ class PersonView(MasterView): 'full_name': {'active': True}, } + form_fields = [ + 'full_name', + 'first_name', + 'middle_name', + 'last_name', + 'users', + ] + def configure_grid(self, g): """ """ super().configure_grid(g) @@ -80,20 +87,54 @@ class PersonView(MasterView): super().configure_form(f) person = f.model_instance - # TODO: master should handle these? (nullable column) - f.set_required('first_name', False) - f.set_required('middle_name', False) - f.set_required('last_name', False) + # full_name + if self.creating or self.editing: + f.remove('full_name') # users - # nb. colanderalchemy wants to do some magic for the true - # 'users' relationship, so we use a different field name - f.remove('users') - if not (self.creating or self.editing): - f.append('_users') - f.set_readonly('_users') - f.set_node('_users', UserRefs(self.request)) - f.set_default('_users', [u.uuid for u in person.users]) + if self.viewing: + f.set_grid('users', self.make_users_grid(person)) + + def make_users_grid(self, person): + """ + Make and return the grid for the Users field. + + This grid is shown for the Users field when viewing a Person. + + :returns: Fully configured :class:`~wuttaweb.grids.base.Grid` + instance. + """ + model = self.app.model + route_prefix = self.get_route_prefix() + + grid = self.make_grid(key=f'{route_prefix}.view.users', + model_class=model.User, + data=person.users, + columns=[ + 'username', + 'active', + ]) + + if self.request.has_perm('users.view'): + url = lambda user, i: self.request.route_url('users.view', uuid=user.uuid) + grid.add_action('view', icon='eye', url=url) + grid.set_link('username') + + if self.request.has_perm('users.edit'): + url = lambda user, i: self.request.route_url('users.edit', uuid=user.uuid) + grid.add_action('edit', url=url) + + return grid + + def objectify(self, form): + """ """ + person = super().objectify(form) + + # full_name + person.full_name = self.app.make_full_name(person.first_name, + person.last_name) + + return person def autocomplete_query(self, term): """ """ diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py index 5eac845..760152d 100644 --- a/src/wuttaweb/views/users.py +++ b/src/wuttaweb/views/users.py @@ -2,7 +2,7 @@ ################################################################################ # # wuttaweb -- Web App for Wutta Framework -# Copyright © 2024 Lance Edgar +# Copyright © 2024-2025 Lance Edgar # # This file is part of Wutta Framework. # @@ -30,7 +30,6 @@ 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 class UserView(MasterView): @@ -61,6 +60,14 @@ class UserView(MasterView): } sort_defaults = 'username' + form_fields = [ + 'username', + 'person', + 'active', + 'prevent_edit', + 'roles', + ] + def get_query(self, session=None): """ """ query = super().get_query(session=session) @@ -109,17 +116,24 @@ class UserView(MasterView): super().configure_form(f) user = f.model_instance - # never show these - f.remove('person_uuid', - 'role_refs') - - # person - f.set_node('person', PersonRef(self.request, empty_option=True)) - f.set_required('person', False) - # username f.set_validator('username', self.unique_username) + # person + if self.creating or self.editing: + f.fields.insert_after('person', 'first_name') + f.set_required('first_name', False) + f.fields.insert_after('first_name', 'last_name') + f.set_required('last_name', False) + f.remove('person') + if self.editing: + person = user.person + if person: + f.set_default('first_name', person.first_name) + f.set_default('last_name', person.last_name) + else: + f.set_node('person', PersonRef(self.request)) + # password # nb. we must avoid 'password' as field name since # ColanderAlchemy wants to handle the raw/hashed value @@ -140,7 +154,7 @@ class UserView(MasterView): def unique_username(self, node, value): """ """ model = self.app.model - session = Session() + session = self.Session() query = session.query(model.User)\ .filter(model.User.username == value) @@ -152,26 +166,48 @@ class UserView(MasterView): if query.count(): node.raise_invalid("Username must be unique") - def objectify(self, form, session=None): + def objectify(self, form): """ """ + model = self.app.model + auth = self.app.get_auth_handler() data = form.validated # normal logic first user = super().objectify(form) + # maybe update person name + if 'first_name' in form or 'last_name' in form: + first_name = data.get('first_name') + last_name = data.get('last_name') + if self.creating and (first_name or last_name): + user.person = auth.make_person(first_name=first_name, last_name=last_name) + elif self.editing: + if first_name or last_name: + if user.person: + person = user.person + if 'first_name' in form: + person.first_name = first_name + if 'last_name' in form: + person.last_name = last_name + person.full_name = self.app.make_full_name(person.first_name, + person.last_name) + else: + user.person = auth.make_person(first_name=first_name, last_name=last_name) + elif user.person: + user.person = None + # 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'): - self.update_roles(user, form, session=session) + self.update_roles(user, form) return user - def update_roles(self, user, form, session=None): + def update_roles(self, user, form): """ """ # TODO # if not self.has_perm('edit_roles'): @@ -181,7 +217,7 @@ class UserView(MasterView): return model = self.app.model - session = session or Session() + session = self.Session() auth = self.app.get_auth_handler() old_roles = set([role.uuid for role in user.roles]) diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py index 3187161..6914f61 100644 --- a/tests/forms/test_schema.py +++ b/tests/forms/test_schema.py @@ -377,20 +377,6 @@ class TestUserRef(WebTestCase): self.assertIn(f'/users/{user.uuid}', url) -class TestUserRefs(DataTestCase): - - def setUp(self): - self.setup_db() - self.request = testing.DummyRequest(wutta_config=self.config) - - def test_widget_maker(self): - model = self.app.model - with patch.object(mod, 'Session', return_value=self.session): - typ = mod.UserRefs(self.request) - widget = typ.widget_maker() - self.assertIsInstance(widget, widgets.UserRefsWidget) - - class TestRoleRefs(DataTestCase): def setUp(self): diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index e571b88..cb42446 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -11,7 +11,7 @@ from pyramid import testing from wuttaweb import grids from wuttaweb.forms import widgets as mod from wuttaweb.forms import schema -from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, UserRefs, Permissions, +from wuttaweb.forms.schema import (FileDownload, PersonRef, RoleRefs, Permissions, WuttaDateTime, EmailRecipients) from wuttaweb.testing import WebTestCase @@ -300,51 +300,6 @@ class TestRoleRefsWidget(WebTestCase): self.assertIn(str(blokes.uuid.hex), html) -class TestUserRefsWidget(WebTestCase): - - def make_field(self, node, **kwargs): - # TODO: not sure why default renderer is in use even though - # pyramid_deform was included in setup? but this works.. - kwargs.setdefault('renderer', deform.Form.default_renderer) - return deform.Field(node, **kwargs) - - def test_serialize(self): - model = self.app.model - - # nb. we let the field construct the widget via our type - # node = colander.SchemaNode(UserRefs(self.request, session=self.session)) - with patch.object(schema, 'Session', return_value=self.session): - node = colander.SchemaNode(UserRefs(self.request)) - field = self.make_field(node) - widget = field.widget - - # readonly is required - self.assertRaises(NotImplementedError, widget.serialize, field, set()) - self.assertRaises(NotImplementedError, widget.serialize, field, set(), readonly=False) - - # empty - html = widget.serialize(field, set(), readonly=True) - self.assertEqual(html, '') - - # with data, no actions - user = model.User(username='barney') - self.session.add(user) - self.session.commit() - html = widget.serialize(field, {user.uuid}, readonly=True) - self.assertIn('