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

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 configure_form(self, f): 

99 """ """ 

100 super().configure_form(f) 

101 user = f.model_instance 

102 

103 # never show these 

104 f.remove('person_uuid', 

105 'role_refs') 

106 

107 # person 

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

109 f.set_required('person', False) 

110 

111 # username 

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

113 

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

124 

125 # roles 

126 f.append('roles') 

127 f.set_node('roles', RoleRefs(self.request)) 

128 

129 if not self.creating: 

130 f.set_default('roles', [role.uuid for role in user.roles]) 

131 

132 def unique_username(self, node, value): 

133 """ """ 

134 model = self.app.model 

135 session = Session() 

136 

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

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

139 

140 if self.editing: 

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

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

143 

144 if query.count(): 

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

146 

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

148 """ """ 

149 data = form.validated 

150 

151 # normal logic first 

152 user = super().objectify(form) 

153 

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

158 

159 # update roles for user 

160 # TODO 

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

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

163 

164 return user 

165 

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 

174 

175 model = self.app.model 

176 session = session or Session() 

177 auth = self.app.get_auth_handler() 

178 

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

180 new_roles = data['roles'] 

181 

182 admin = auth.get_role_administrator(session) 

183 ignored = { 

184 auth.get_role_authenticated(session).uuid, 

185 auth.get_role_anonymous(session).uuid, 

186 } 

187 

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) 

199 

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) 

209 

210 @classmethod 

211 def defaults(cls, config): 

212 """ """ 

213 

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 

218 

219 cls._defaults(config) 

220 

221 

222def defaults(config, **kwargs): 

223 base = globals() 

224 

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

226 UserView.defaults(config) 

227 

228 

229def includeme(config): 

230 defaults(config)