Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/users.py: 100%
99 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 22:41 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 22:41 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Views for users
25"""
27import colander
29from wuttjamaican.db.model import User
30from wuttaweb.views import MasterView
31from wuttaweb.forms import widgets
32from wuttaweb.forms.schema import PersonRef, RoleRefs
33from wuttaweb.db import Session
36class UserView(MasterView):
37 """
38 Master view for users.
40 Default route prefix is ``users``.
42 Notable URLs provided by this class:
44 * ``/users/``
45 * ``/users/new``
46 * ``/users/XXX``
47 * ``/users/XXX/edit``
48 * ``/users/XXX/delete``
49 """
50 model_class = User
52 grid_columns = [
53 'username',
54 'person',
55 'active',
56 ]
58 filter_defaults = {
59 'username': {'active': True},
60 'active': {'active': True, 'verb': 'is_true'},
61 }
62 sort_defaults = 'username'
64 def get_query(self, session=None):
65 """ """
66 query = super().get_query(session=session)
68 # nb. always join Person
69 model = self.app.model
70 query = query.outerjoin(model.Person)
72 return query
74 def configure_grid(self, g):
75 """ """
76 super().configure_grid(g)
77 model = self.app.model
79 # never show these
80 g.remove('person_uuid',
81 'role_refs',
82 'password')
84 # username
85 g.set_link('username')
87 # person
88 g.set_link('person')
89 g.set_sorter('person', model.Person.full_name)
90 g.set_filter('person', model.Person.full_name,
91 label="Person Full Name")
93 def grid_row_class(self, user, data, i):
94 """ """
95 if not user.active:
96 return 'has-background-warning'
98 def configure_form(self, f):
99 """ """
100 super().configure_form(f)
101 user = f.model_instance
103 # never show these
104 f.remove('person_uuid',
105 'role_refs')
107 # person
108 f.set_node('person', PersonRef(self.request, empty_option=True))
109 f.set_required('person', False)
111 # username
112 f.set_validator('username', self.unique_username)
114 # password
115 # nb. we must avoid 'password' as field name since
116 # ColanderAlchemy wants to handle the raw/hashed value
117 f.remove('password')
118 # nb. no need for password field if readonly
119 if self.creating or self.editing:
120 # nb. use 'set_password' as field name
121 f.append('set_password')
122 f.set_required('set_password', False)
123 f.set_widget('set_password', widgets.CheckedPasswordWidget())
125 # roles
126 f.append('roles')
127 f.set_node('roles', RoleRefs(self.request))
129 if not self.creating:
130 f.set_default('roles', [role.uuid for role in user.roles])
132 def unique_username(self, node, value):
133 """ """
134 model = self.app.model
135 session = Session()
137 query = session.query(model.User)\
138 .filter(model.User.username == value)
140 if self.editing:
141 uuid = self.request.matchdict['uuid']
142 query = query.filter(model.User.uuid != uuid)
144 if query.count():
145 node.raise_invalid("Username must be unique")
147 def objectify(self, form, session=None):
148 """ """
149 data = form.validated
151 # normal logic first
152 user = super().objectify(form)
154 # maybe set user password
155 if 'set_password' in form and data.get('set_password'):
156 auth = self.app.get_auth_handler()
157 auth.set_user_password(user, data['set_password'])
159 # update roles for user
160 # TODO
161 # if self.has_perm('edit_roles'):
162 self.update_roles(user, form, session=session)
164 return user
166 def update_roles(self, user, form, session=None):
167 """ """
168 # TODO
169 # if not self.has_perm('edit_roles'):
170 # return
171 data = form.validated
172 if 'roles' not in data:
173 return
175 model = self.app.model
176 session = session or Session()
177 auth = self.app.get_auth_handler()
179 old_roles = set([role.uuid for role in user.roles])
180 new_roles = data['roles']
182 admin = auth.get_role_administrator(session)
183 ignored = {
184 auth.get_role_authenticated(session).uuid,
185 auth.get_role_anonymous(session).uuid,
186 }
188 # add any new roles for the user, taking care to avoid certain
189 # unwanted operations for built-in roles
190 for uuid in new_roles:
191 if uuid in ignored:
192 continue
193 if uuid in old_roles:
194 continue
195 if uuid == admin.uuid and not self.request.is_root:
196 continue
197 role = session.get(model.Role, uuid)
198 user.roles.append(role)
200 # remove any roles which were *not* specified, taking care to
201 # avoid certain unwanted operations for built-in roles
202 for uuid in old_roles:
203 if uuid in new_roles:
204 continue
205 if uuid == admin.uuid and not self.request.is_root:
206 continue
207 role = session.get(model.Role, uuid)
208 user.roles.remove(role)
210 @classmethod
211 def defaults(cls, config):
212 """ """
214 # nb. User may come from custom model
215 wutta_config = config.registry.settings['wutta_config']
216 app = wutta_config.get_app()
217 cls.model_class = app.model.User
219 cls._defaults(config)
222def defaults(config, **kwargs):
223 base = globals()
225 UserView = kwargs.get('UserView', base['UserView'])
226 UserView.defaults(config)
229def includeme(config):
230 defaults(config)