Coverage for src/sideshow/web/forms/widgets.py: 0%
174 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-04 16:11 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-04 16:11 -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"""
24Form widgets
26This module defines some custom widgets for use with WuttaWeb.
28However for convenience it also makes other Deform widgets available
29in the namespace:
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.DateTimeInputWidget`
40* :class:`deform:deform.widget.MoneyInputWidget`
41"""
43import datetime
44import decimal
45import os
47import colander
48import humanize
49from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
50 PasswordWidget, CheckedPasswordWidget,
51 CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
52 DateTimeInputWidget, MoneyInputWidget)
53from webhelpers2.html import HTML
55from wuttjamaican.conf import parse_list
57from wuttaweb.db import Session
58from wuttaweb.grids import Grid
61class ObjectRefWidget(SelectWidget):
62 """
63 Widget for use with model "object reference" fields, e.g. foreign
64 key UUID => TargetModel instance.
66 While you may create instances of this widget directly, it
67 normally happens automatically when schema nodes of the
68 :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of
69 the form schema; via
70 :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`.
72 In readonly mode, this renders a ``<span>`` tag around the
73 :attr:`model_instance` (converted to string).
75 Otherwise it renders a select (dropdown) element allowing user to
76 choose from available records.
78 This is a subclass of :class:`deform:deform.widget.SelectWidget`
79 and uses these Deform templates:
81 * ``select``
82 * ``readonly/objectref``
84 .. attribute:: model_instance
86 Reference to the model record instance, i.e. the "far side" of
87 the foreign key relationship.
89 .. note::
91 You do not need to provide the ``model_instance`` when
92 constructing the widget. Rather, it is set automatically
93 when the :class:`~wuttaweb.forms.schema.ObjectRef` type
94 instance (associated with the node) is serialized.
95 """
96 readonly_template = 'readonly/objectref'
98 def __init__(self, request, url=None, *args, **kwargs):
99 super().__init__(*args, **kwargs)
100 self.request = request
101 self.url = url
103 def get_template_values(self, field, cstruct, kw):
104 """ """
105 values = super().get_template_values(field, cstruct, kw)
107 # add url, only if rendering readonly
108 readonly = kw.get('readonly', self.readonly)
109 if readonly:
110 if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None):
111 values['url'] = self.url(field.schema.model_instance)
113 return values
116class NotesWidget(TextAreaWidget):
117 """
118 Widget for use with "notes" fields.
120 In readonly mode, this shows the notes with a background to make
121 them stand out a bit more.
123 Otherwise it effectively shows a ``<textarea>`` input element.
125 This is a subclass of :class:`deform:deform.widget.TextAreaWidget`
126 and uses these Deform templates:
128 * ``textarea``
129 * ``readonly/notes``
130 """
131 readonly_template = 'readonly/notes'
134class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
135 """
136 Custom widget for :class:`python:set` fields.
138 This is a subclass of
139 :class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
140 Wutta-related params to the constructor.
142 :param request: Current :term:`request` object.
144 :param session: Optional :term:`db session` to use instead of
145 :class:`wuttaweb.db.sess.Session`.
147 It uses these Deform templates:
149 * ``checkbox_choice``
150 * ``readonly/checkbox_choice``
151 """
153 def __init__(self, request, session=None, *args, **kwargs):
154 super().__init__(*args, **kwargs)
155 self.request = request
156 self.config = self.request.wutta_config
157 self.app = self.config.get_app()
158 self.session = session or Session()
161class WuttaDateTimeWidget(DateTimeInputWidget):
162 """
163 Custom widget for :class:`python:datetime.datetime` fields.
165 The main purpose of this widget is to leverage
166 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
167 for the readonly display.
169 It is automatically used for SQLAlchemy mapped classes where the
170 field maps to a :class:`sqlalchemy:sqlalchemy.types.DateTime`
171 column. For other (non-mapped) datetime fields, you may have to
172 use it explicitly via
173 :meth:`~wuttaweb.forms.base.Form.set_widget()`.
175 This is a subclass of
176 :class:`deform:deform.widget.DateTimeInputWidget` and uses these
177 Deform templates:
179 * ``datetimeinput``
180 """
182 def __init__(self, request, *args, **kwargs):
183 super().__init__(*args, **kwargs)
184 self.request = request
185 self.config = self.request.wutta_config
186 self.app = self.config.get_app()
188 def serialize(self, field, cstruct, **kw):
189 """ """
190 readonly = kw.get('readonly', self.readonly)
191 if readonly and cstruct:
192 dt = datetime.datetime.fromisoformat(cstruct)
193 return self.app.render_datetime(dt)
195 return super().serialize(field, cstruct, **kw)
198class WuttaMoneyInputWidget(MoneyInputWidget):
199 """
200 Custom widget for "money" fields. This is used by default for
201 :class:`~wuttaweb.forms.schema.WuttaMoney` type nodes.
203 The main purpose of this widget is to leverage
204 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()`
205 for the readonly display.
207 This is a subclass of
208 :class:`deform:deform.widget.MoneyInputWidget` and uses these
209 Deform templates:
211 * ``moneyinput``
213 :param request: Current :term:`request` object.
214 """
216 def __init__(self, request, *args, **kwargs):
217 super().__init__(*args, **kwargs)
218 self.request = request
219 self.config = self.request.wutta_config
220 self.app = self.config.get_app()
222 def serialize(self, field, cstruct, **kw):
223 """ """
224 readonly = kw.get('readonly', self.readonly)
225 if readonly:
226 if cstruct in (colander.null, None):
227 return HTML.tag('span')
228 cstruct = decimal.Decimal(cstruct)
229 return HTML.tag('span', c=[self.app.render_currency(cstruct)])
231 return super().serialize(field, cstruct, **kw)
234class FileDownloadWidget(Widget):
235 """
236 Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
237 fields.
239 This only supports readonly, and shows a hyperlink to download the
240 file. Link text is the filename plus file size.
242 This is a subclass of :class:`deform:deform.widget.Widget` and
243 uses these Deform templates:
245 * ``readonly/filedownload``
247 :param request: Current :term:`request` object.
249 :param url: Optional URL for hyperlink. If not specified, file
250 name/size is shown with no hyperlink.
251 """
252 readonly_template = 'readonly/filedownload'
254 def __init__(self, request, *args, **kwargs):
255 self.url = kwargs.pop('url', None)
256 super().__init__(*args, **kwargs)
257 self.request = request
258 self.config = self.request.wutta_config
259 self.app = self.config.get_app()
261 def serialize(self, field, cstruct, **kw):
262 """ """
263 # nb. readonly is the only way this rolls
264 kw['readonly'] = True
265 template = self.readonly_template
267 path = cstruct or None
268 if path:
269 kw.setdefault('filename', os.path.basename(path))
270 kw.setdefault('filesize', self.readable_size(path))
271 if self.url:
272 kw.setdefault('url', self.url)
274 else:
275 kw.setdefault('filename', None)
276 kw.setdefault('filesize', None)
278 kw.setdefault('url', None)
279 values = self.get_template_values(field, cstruct, kw)
280 return field.renderer(template, **values)
282 def readable_size(self, path):
283 """ """
284 try:
285 size = os.path.getsize(path)
286 except os.error:
287 size = 0
288 return humanize.naturalsize(size)
291class GridWidget(Widget):
292 """
293 Widget for fields whose data is represented by a :term:`grid`.
295 This is a subclass of :class:`deform:deform.widget.Widget` but
296 does not use any Deform templates.
298 This widget only supports "readonly" mode, is not editable. It is
299 merely a convenience around the grid itself, which does the heavy
300 lifting.
302 Instead of creating this widget directly you probably should call
303 :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form.
305 :param request: Current :term:`request` object.
307 :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to
308 display the field data.
309 """
311 def __init__(self, request, grid, *args, **kwargs):
312 super().__init__(*args, **kwargs)
313 self.request = request
314 self.grid = grid
316 def serialize(self, field, cstruct, **kw):
317 """
318 This widget simply calls
319 :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
320 the ``grid`` to serialize.
321 """
322 readonly = kw.get('readonly', self.readonly)
323 if not readonly:
324 raise NotImplementedError("edit not allowed for this widget")
326 return self.grid.render_table_element()
329class RoleRefsWidget(WuttaCheckboxChoiceWidget):
330 """
331 Widget for use with User
332 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
333 This is the default widget for the
334 :class:`~wuttaweb.forms.schema.RoleRefs` type.
336 This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
337 """
338 readonly_template = 'readonly/rolerefs'
340 def serialize(self, field, cstruct, **kw):
341 """ """
342 model = self.app.model
344 # special logic when field is editable
345 readonly = kw.get('readonly', self.readonly)
346 if not readonly:
348 # but does not apply if current user is root
349 if not self.request.is_root:
350 auth = self.app.get_auth_handler()
351 admin = auth.get_role_administrator(self.session)
353 # prune admin role from values list; it should not be
354 # one of the options since current user is not admin
355 values = kw.get('values', self.values)
356 values = [val for val in values
357 if val[0] != admin.uuid]
358 kw['values'] = values
360 else: # readonly
362 # roles
363 roles = []
364 if cstruct:
365 for uuid in cstruct:
366 role = self.session.get(model.Role, uuid)
367 if role:
368 roles.append(role)
369 kw['roles'] = roles
371 # url
372 url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
373 kw['url'] = url
375 # default logic from here
376 return super().serialize(field, cstruct, **kw)
379class UserRefsWidget(WuttaCheckboxChoiceWidget):
380 """
381 Widget for use with Role
382 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field.
383 This is the default widget for the
384 :class:`~wuttaweb.forms.schema.UserRefs` type.
386 This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however
387 it only supports readonly mode and does not use a template.
388 Rather, it generates and renders a
389 :class:`~wuttaweb.grids.base.Grid` showing the users list.
390 """
392 def serialize(self, field, cstruct, **kw):
393 """ """
394 readonly = kw.get('readonly', self.readonly)
395 if not readonly:
396 raise NotImplementedError("edit not allowed for this widget")
398 model = self.app.model
399 columns = ['username', 'active']
401 # generate data set for users
402 users = []
403 if cstruct:
404 for uuid in cstruct:
405 user = self.session.get(model.User, uuid)
406 if user:
407 users.append(dict([(key, getattr(user, key))
408 for key in columns + ['uuid']]))
410 # do not render if no data
411 if not users:
412 return HTML.tag('span')
414 # grid
415 grid = Grid(self.request, key='roles.view.users',
416 columns=columns, data=users)
418 # view action
419 if self.request.has_perm('users.view'):
420 url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid'])
421 grid.add_action('view', icon='eye', url=url)
422 grid.set_link('person')
423 grid.set_link('username')
425 # edit action
426 if self.request.has_perm('users.edit'):
427 url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid'])
428 grid.add_action('edit', url=url)
430 # render as simple <b-table>
431 # nb. must indicate we are a part of this form
432 form = getattr(field.parent, 'wutta_form', None)
433 return grid.render_table_element(form)
436class PermissionsWidget(WuttaCheckboxChoiceWidget):
437 """
438 Widget for use with Role
439 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
440 field.
442 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses
443 these Deform templates:
445 * ``permissions``
446 * ``readonly/permissions``
447 """
448 template = 'permissions'
449 readonly_template = 'readonly/permissions'
451 def serialize(self, field, cstruct, **kw):
452 """ """
453 kw.setdefault('permissions', self.permissions)
455 if 'values' not in kw:
456 values = []
457 for gkey, group in self.permissions.items():
458 for pkey, perm in group['perms'].items():
459 values.append((pkey, perm['label']))
460 kw['values'] = values
462 return super().serialize(field, cstruct, **kw)
465class EmailRecipientsWidget(TextAreaWidget):
466 """
467 Widget for :term:`email setting` recipient fields (``To``, ``Cc``,
468 ``Bcc``).
470 This is a subclass of
471 :class:`deform:deform.widget.TextAreaWidget`. It uses these
472 Deform templates:
474 * ``textarea``
475 * ``readonly/email_recips``
477 See also the :class:`~wuttaweb.forms.schema.EmailRecipients`
478 schema type, which uses this widget.
479 """
480 readonly_template = 'readonly/email_recips'
482 def serialize(self, field, cstruct, **kw):
483 """ """
484 readonly = kw.get('readonly', self.readonly)
485 if readonly:
486 kw['recips'] = parse_list(cstruct or '')
488 return super().serialize(field, cstruct, **kw)
490 def deserialize(self, field, pstruct):
491 """ """
492 if pstruct is colander.null:
493 return colander.null
495 values = [value for value in parse_list(pstruct)
496 if value]
497 return ', '.join(values)
500class BatchIdWidget(Widget):
501 """
502 Widget for use with the
503 :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
504 field of a :term:`batch` model.
506 This widget is "always" read-only and renders the Batch ID as
507 zero-padded 8-char string
508 """
510 def serialize(self, field, cstruct, **kw):
511 """ """
512 if cstruct is colander.null:
513 return colander.null
515 batch_id = int(cstruct)
516 return f'{batch_id:08d}'