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

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""" 

26 

27import colander 

28 

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 

34 

35 

36class UserView(MasterView): 

37 """ 

38 Master view for users. 

39 

40 Default route prefix is ``users``. 

41 

42 Notable URLs provided by this class: 

43 

44 * ``/users/`` 

45 * ``/users/new`` 

46 * ``/users/XXX`` 

47 * ``/users/XXX/edit`` 

48 * ``/users/XXX/delete`` 

49 """ 

50 model_class = User 

51 

52 grid_columns = [ 

53 'username', 

54 'person', 

55 'active', 

56 ] 

57 

58 filter_defaults = { 

59 'username': {'active': True}, 

60 'active': {'active': True, 'verb': 'is_true'}, 

61 } 

62 sort_defaults = 'username' 

63 

64 def get_query(self, session=None): 

65 """ """ 

66 query = super().get_query(session=session) 

67 

68 # nb. always join Person 

69 model = self.app.model 

70 query = query.outerjoin(model.Person) 

71 

72 return query 

73 

74 def configure_grid(self, g): 

75 """ """ 

76 super().configure_grid(g) 

77 model = self.app.model 

78 

79 # never show these 

80 g.remove('person_uuid', 

81 'role_refs', 

82 'password') 

83 

84 # username 

85 g.set_link('username') 

86 

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") 

92 

93 def grid_row_class(self, user, data, i): 

94 """ """ 

95 if not user.active: 

96 return 'has-background-warning' 

97 

98 def is_editable(self, user): 

99 """ """ 

100 

101 # only root can edit certain users 

102 if user.prevent_edit and not self.request.is_root: 

103 return False 

104 

105 return True 

106 

107 def configure_form(self, f): 

108 """ """ 

109 super().configure_form(f) 

110 user = f.model_instance 

111 

112 # never show these 

113 f.remove('person_uuid', 

114 'role_refs') 

115 

116 # person 

117 f.set_node('person', PersonRef(self.request, empty_option=True)) 

118 f.set_required('person', False) 

119 

120 # username 

121 f.set_validator('username', self.unique_username) 

122 

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()) 

133 

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]) 

139 

140 def unique_username(self, node, value): 

141 """ """ 

142 model = self.app.model 

143 session = Session() 

144 

145 query = session.query(model.User)\ 

146 .filter(model.User.username == value) 

147 

148 if self.editing: 

149 uuid = self.request.matchdict['uuid'] 

150 query = query.filter(model.User.uuid != uuid) 

151 

152 if query.count(): 

153 node.raise_invalid("Username must be unique") 

154 

155 def objectify(self, form, session=None): 

156 """ """ 

157 data = form.validated 

158 

159 # normal logic first 

160 user = super().objectify(form) 

161 

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']) 

166 

167 # update roles for user 

168 # TODO 

169 # if self.has_perm('edit_roles'): 

170 self.update_roles(user, form, session=session) 

171 

172 return user 

173 

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 

182 

183 model = self.app.model 

184 session = session or Session() 

185 auth = self.app.get_auth_handler() 

186 

187 old_roles = set([role.uuid for role in user.roles]) 

188 new_roles = data['roles'] 

189 

190 admin = auth.get_role_administrator(session) 

191 ignored = { 

192 auth.get_role_authenticated(session).uuid, 

193 auth.get_role_anonymous(session).uuid, 

194 } 

195 

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) 

207 

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) 

217 

218 @classmethod 

219 def defaults(cls, config): 

220 """ """ 

221 

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 

226 

227 cls._defaults(config) 

228 

229 

230def defaults(config, **kwargs): 

231 base = globals() 

232 

233 UserView = kwargs.get('UserView', base['UserView']) 

234 UserView.defaults(config) 

235 

236 

237def includeme(config): 

238 defaults(config)