Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/forms/widgets.py: 100%

117 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-27 21:18 -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""" 

24Form widgets 

25 

26This module defines some custom widgets for use with WuttaWeb. 

27 

28However for convenience it also makes other Deform widgets available 

29in the namespace: 

30 

31* :class:`deform:deform.widget.Widget` (base class) 

32* :class:`deform:deform.widget.TextInputWidget` 

33* :class:`deform:deform.widget.TextAreaWidget` 

34* :class:`deform:deform.widget.PasswordWidget` 

35* :class:`deform:deform.widget.CheckedPasswordWidget` 

36* :class:`deform:deform.widget.CheckboxWidget` 

37* :class:`deform:deform.widget.SelectWidget` 

38* :class:`deform:deform.widget.CheckboxChoiceWidget` 

39* :class:`deform:deform.widget.MoneyInputWidget` 

40""" 

41 

42import os 

43 

44import colander 

45import humanize 

46from deform.widget import (Widget, TextInputWidget, TextAreaWidget, 

47 PasswordWidget, CheckedPasswordWidget, 

48 CheckboxWidget, SelectWidget, CheckboxChoiceWidget, 

49 MoneyInputWidget) 

50from webhelpers2.html import HTML 

51 

52from wuttaweb.db import Session 

53from wuttaweb.grids import Grid 

54 

55 

56class ObjectRefWidget(SelectWidget): 

57 """ 

58 Widget for use with model "object reference" fields, e.g. foreign 

59 key UUID => TargetModel instance. 

60 

61 While you may create instances of this widget directly, it 

62 normally happens automatically when schema nodes of the 

63 :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of 

64 the form schema; via 

65 :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`. 

66 

67 In readonly mode, this renders a ``<span>`` tag around the 

68 :attr:`model_instance` (converted to string). 

69 

70 Otherwise it renders a select (dropdown) element allowing user to 

71 choose from available records. 

72 

73 This is a subclass of :class:`deform:deform.widget.SelectWidget` 

74 and uses these Deform templates: 

75 

76 * ``select`` 

77 * ``readonly/objectref`` 

78 

79 .. attribute:: model_instance 

80 

81 Reference to the model record instance, i.e. the "far side" of 

82 the foreign key relationship. 

83 

84 .. note:: 

85 

86 You do not need to provide the ``model_instance`` when 

87 constructing the widget. Rather, it is set automatically 

88 when the :class:`~wuttaweb.forms.schema.ObjectRef` type 

89 instance (associated with the node) is serialized. 

90 """ 

91 readonly_template = 'readonly/objectref' 

92 

93 def __init__(self, request, url=None, *args, **kwargs): 

94 super().__init__(*args, **kwargs) 

95 self.request = request 

96 self.url = url 

97 

98 def get_template_values(self, field, cstruct, kw): 

99 """ """ 

100 values = super().get_template_values(field, cstruct, kw) 

101 

102 # add url, only if rendering readonly 

103 readonly = kw.get('readonly', self.readonly) 

104 if readonly: 

105 if 'url' not in values and self.url and field.schema.model_instance: 

106 values['url'] = self.url(field.schema.model_instance) 

107 

108 return values 

109 

110 

111class NotesWidget(TextAreaWidget): 

112 """ 

113 Widget for use with "notes" fields. 

114 

115 In readonly mode, this shows the notes with a background to make 

116 them stand out a bit more. 

117 

118 Otherwise it effectively shows a ``<textarea>`` input element. 

119 

120 This is a subclass of :class:`deform:deform.widget.TextAreaWidget` 

121 and uses these Deform templates: 

122 

123 * ``textarea`` 

124 * ``readonly/notes`` 

125 """ 

126 readonly_template = 'readonly/notes' 

127 

128 

129class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): 

130 """ 

131 Custom widget for :class:`python:set` fields. 

132 

133 This is a subclass of 

134 :class:`deform:deform.widget.CheckboxChoiceWidget`, but adds 

135 Wutta-related params to the constructor. 

136 

137 :param request: Current :term:`request` object. 

138 

139 :param session: Optional :term:`db session` to use instead of 

140 :class:`wuttaweb.db.sess.Session`. 

141 

142 It uses these Deform templates: 

143 

144 * ``checkbox_choice`` 

145 * ``readonly/checkbox_choice`` 

146 """ 

147 

148 def __init__(self, request, session=None, *args, **kwargs): 

149 super().__init__(*args, **kwargs) 

150 self.request = request 

151 self.config = self.request.wutta_config 

152 self.app = self.config.get_app() 

153 self.session = session or Session() 

154 

155 

156class FileDownloadWidget(Widget): 

157 """ 

158 Widget for use with :class:`~wuttaweb.forms.schema.FileDownload` 

159 fields. 

160 

161 This only supports readonly, and shows a hyperlink to download the 

162 file. Link text is the filename plus file size. 

163 

164 This is a subclass of :class:`deform:deform.widget.Widget` and 

165 uses these Deform templates: 

166 

167 * ``readonly/filedownload`` 

168 

169 :param request: Current :term:`request` object. 

170 

171 :param url: Optional URL for hyperlink. If not specified, file 

172 name/size is shown with no hyperlink. 

173 """ 

174 readonly_template = 'readonly/filedownload' 

175 

176 def __init__(self, request, *args, **kwargs): 

177 self.url = kwargs.pop('url', None) 

178 super().__init__(*args, **kwargs) 

179 self.request = request 

180 self.config = self.request.wutta_config 

181 self.app = self.config.get_app() 

182 

183 def serialize(self, field, cstruct, **kw): 

184 """ """ 

185 # nb. readonly is the only way this rolls 

186 kw['readonly'] = True 

187 template = self.readonly_template 

188 

189 path = cstruct or None 

190 if path: 

191 kw.setdefault('filename', os.path.basename(path)) 

192 kw.setdefault('filesize', self.readable_size(path)) 

193 if self.url: 

194 kw.setdefault('url', self.url) 

195 

196 else: 

197 kw.setdefault('filename', None) 

198 kw.setdefault('filesize', None) 

199 

200 kw.setdefault('url', None) 

201 values = self.get_template_values(field, cstruct, kw) 

202 return field.renderer(template, **values) 

203 

204 def readable_size(self, path): 

205 """ """ 

206 try: 

207 size = os.path.getsize(path) 

208 except os.error: 

209 size = 0 

210 return humanize.naturalsize(size) 

211 

212 

213class RoleRefsWidget(WuttaCheckboxChoiceWidget): 

214 """ 

215 Widget for use with User 

216 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field. 

217 This is the default widget for the 

218 :class:`~wuttaweb.forms.schema.RoleRefs` type. 

219 

220 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. 

221 """ 

222 readonly_template = 'readonly/rolerefs' 

223 

224 def serialize(self, field, cstruct, **kw): 

225 """ """ 

226 model = self.app.model 

227 

228 # special logic when field is editable 

229 readonly = kw.get('readonly', self.readonly) 

230 if not readonly: 

231 

232 # but does not apply if current user is root 

233 if not self.request.is_root: 

234 auth = self.app.get_auth_handler() 

235 admin = auth.get_role_administrator(self.session) 

236 

237 # prune admin role from values list; it should not be 

238 # one of the options since current user is not admin 

239 values = kw.get('values', self.values) 

240 values = [val for val in values 

241 if val[0] != admin.uuid] 

242 kw['values'] = values 

243 

244 else: # readonly 

245 

246 # roles 

247 roles = [] 

248 if cstruct: 

249 for uuid in cstruct: 

250 role = self.session.get(model.Role, uuid) 

251 if role: 

252 roles.append(role) 

253 kw['roles'] = roles 

254 

255 # url 

256 url = lambda role: self.request.route_url('roles.view', uuid=role.uuid) 

257 kw['url'] = url 

258 

259 # default logic from here 

260 return super().serialize(field, cstruct, **kw) 

261 

262 

263class UserRefsWidget(WuttaCheckboxChoiceWidget): 

264 """ 

265 Widget for use with Role 

266 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field. 

267 This is the default widget for the 

268 :class:`~wuttaweb.forms.schema.UserRefs` type. 

269 

270 This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however 

271 it only supports readonly mode and does not use a template. 

272 Rather, it generates and renders a 

273 :class:`~wuttaweb.grids.base.Grid` showing the users list. 

274 """ 

275 

276 def serialize(self, field, cstruct, **kw): 

277 """ """ 

278 readonly = kw.get('readonly', self.readonly) 

279 if not readonly: 

280 raise NotImplementedError("edit not allowed for this widget") 

281 

282 model = self.app.model 

283 columns = ['person', 'username', 'active'] 

284 

285 # generate data set for users 

286 users = [] 

287 if cstruct: 

288 for uuid in cstruct: 

289 user = self.session.get(model.User, uuid) 

290 if user: 

291 users.append(dict([(key, getattr(user, key)) 

292 for key in columns + ['uuid']])) 

293 

294 # do not render if no data 

295 if not users: 

296 return HTML.tag('span') 

297 

298 # grid 

299 grid = Grid(self.request, key='roles.view.users', 

300 columns=columns, data=users) 

301 

302 # view action 

303 if self.request.has_perm('users.view'): 

304 url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid']) 

305 grid.add_action('view', icon='eye', url=url) 

306 grid.set_link('person') 

307 grid.set_link('username') 

308 

309 # edit action 

310 if self.request.has_perm('users.edit'): 

311 url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid']) 

312 grid.add_action('edit', url=url) 

313 

314 # render as simple <b-table> 

315 # nb. must indicate we are a part of this form 

316 form = getattr(field.parent, 'wutta_form', None) 

317 return grid.render_table_element(form) 

318 

319 

320class PermissionsWidget(WuttaCheckboxChoiceWidget): 

321 """ 

322 Widget for use with Role 

323 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions` 

324 field. 

325 

326 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses 

327 these Deform templates: 

328 

329 * ``permissions`` 

330 * ``readonly/permissions`` 

331 """ 

332 template = 'permissions' 

333 readonly_template = 'readonly/permissions' 

334 

335 def serialize(self, field, cstruct, **kw): 

336 """ """ 

337 kw.setdefault('permissions', self.permissions) 

338 

339 if 'values' not in kw: 

340 values = [] 

341 for gkey, group in self.permissions.items(): 

342 for pkey, perm in group['perms'].items(): 

343 values.append((pkey, perm['label'])) 

344 kw['values'] = values 

345 

346 return super().serialize(field, cstruct, **kw)