Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/users.py: 100%
103 statements
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-28 21:19 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-28 21:19 -0600
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 is_editable(self, user):
99 """ """
101 # only root can edit certain users
102 if user.prevent_edit and not self.request.is_root:
103 return False
105 return True
107 def configure_form(self, f):
108 """ """
109 super().configure_form(f)
110 user = f.model_instance
112 # never show these
113 f.remove('person_uuid',
114 'role_refs')
116 # person
117 f.set_node('person', PersonRef(self.request, empty_option=True))
118 f.set_required('person', False)
120 # username
121 f.set_validator('username', self.unique_username)
123 # password
124 # nb. we must avoid 'password' as field name since
125 # ColanderAlchemy wants to handle the raw/hashed value
126 f.remove('password')
127 # nb. no need for password field if readonly
128 if self.creating or self.editing:
129 # nb. use 'set_password' as field name
130 f.append('set_password')
131 f.set_required('set_password', False)
132 f.set_widget('set_password', widgets.CheckedPasswordWidget())
134 # roles
135 f.append('roles')
136 f.set_node('roles', RoleRefs(self.request))
137 if not self.creating:
138 f.set_default('roles', [role.uuid.hex for role in user.roles])
140 def unique_username(self, node, value):
141 """ """
142 model = self.app.model
143 session = Session()
145 query = session.query(model.User)\
146 .filter(model.User.username == value)
148 if self.editing:
149 uuid = self.request.matchdict['uuid']
150 query = query.filter(model.User.uuid != uuid)
152 if query.count():
153 node.raise_invalid("Username must be unique")
155 def objectify(self, form, session=None):
156 """ """
157 data = form.validated
159 # normal logic first
160 user = super().objectify(form)
162 # maybe set user password
163 if 'set_password' in form and data.get('set_password'):
164 auth = self.app.get_auth_handler()
165 auth.set_user_password(user, data['set_password'])
167 # update roles for user
168 # TODO
169 # if self.has_perm('edit_roles'):
170 self.update_roles(user, form, session=session)
172 return user
174 def update_roles(self, user, form, session=None):
175 """ """
176 # TODO
177 # if not self.has_perm('edit_roles'):
178 # return
179 data = form.validated
180 if 'roles' not in data:
181 return
183 model = self.app.model
184 session = session or Session()
185 auth = self.app.get_auth_handler()
187 old_roles = set([role.uuid for role in user.roles])
188 new_roles = data['roles']
190 admin = auth.get_role_administrator(session)
191 ignored = {
192 auth.get_role_authenticated(session).uuid,
193 auth.get_role_anonymous(session).uuid,
194 }
196 # add any new roles for the user, taking care to avoid certain
197 # unwanted operations for built-in roles
198 for uuid in new_roles:
199 if uuid in ignored:
200 continue
201 if uuid in old_roles:
202 continue
203 if uuid == admin.uuid and not self.request.is_root:
204 continue
205 role = session.get(model.Role, uuid)
206 user.roles.append(role)
208 # remove any roles which were *not* specified, taking care to
209 # avoid certain unwanted operations for built-in roles
210 for uuid in old_roles:
211 if uuid in new_roles:
212 continue
213 if uuid == admin.uuid and not self.request.is_root:
214 continue
215 role = session.get(model.Role, uuid)
216 user.roles.remove(role)
218 @classmethod
219 def defaults(cls, config):
220 """ """
222 # nb. User may come from custom model
223 wutta_config = config.registry.settings['wutta_config']
224 app = wutta_config.get_app()
225 cls.model_class = app.model.User
227 cls._defaults(config)
230def defaults(config, **kwargs):
231 base = globals()
233 UserView = kwargs.get('UserView', base['UserView'])
234 UserView.defaults(config)
237def includeme(config):
238 defaults(config)