Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/forms/widgets.py: 100%
187 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-15 08:54 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-15 08:54 -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.DateInputWidget`
40* :class:`deform:deform.widget.DateTimeInputWidget`
41* :class:`deform:deform.widget.MoneyInputWidget`
42"""
44import datetime
45import decimal
46import os
48import colander
49import humanize
50from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
51 PasswordWidget, CheckedPasswordWidget,
52 CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
53 DateInputWidget, DateTimeInputWidget, MoneyInputWidget)
54from webhelpers2.html import HTML
56from wuttjamaican.conf import parse_list
58from wuttaweb.db import Session
59from wuttaweb.grids import Grid
62class ObjectRefWidget(SelectWidget):
63 """
64 Widget for use with model "object reference" fields, e.g. foreign
65 key UUID => TargetModel instance.
67 While you may create instances of this widget directly, it
68 normally happens automatically when schema nodes of the
69 :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of
70 the form schema; via
71 :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`.
73 In readonly mode, this renders a ``<span>`` tag around the
74 :attr:`model_instance` (converted to string).
76 Otherwise it renders a select (dropdown) element allowing user to
77 choose from available records.
79 This is a subclass of :class:`deform:deform.widget.SelectWidget`
80 and uses these Deform templates:
82 * ``select``
83 * ``readonly/objectref``
85 .. attribute:: model_instance
87 Reference to the model record instance, i.e. the "far side" of
88 the foreign key relationship.
90 .. note::
92 You do not need to provide the ``model_instance`` when
93 constructing the widget. Rather, it is set automatically
94 when the :class:`~wuttaweb.forms.schema.ObjectRef` type
95 instance (associated with the node) is serialized.
96 """
97 readonly_template = 'readonly/objectref'
99 def __init__(self, request, url=None, *args, **kwargs):
100 super().__init__(*args, **kwargs)
101 self.request = request
102 self.url = url
104 def get_template_values(self, field, cstruct, kw):
105 """ """
106 values = super().get_template_values(field, cstruct, kw)
108 # add url, only if rendering readonly
109 readonly = kw.get('readonly', self.readonly)
110 if readonly:
111 if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None):
112 values['url'] = self.url(field.schema.model_instance)
114 return values
117class NotesWidget(TextAreaWidget):
118 """
119 Widget for use with "notes" fields.
121 In readonly mode, this shows the notes with a background to make
122 them stand out a bit more.
124 Otherwise it effectively shows a ``<textarea>`` input element.
126 This is a subclass of :class:`deform:deform.widget.TextAreaWidget`
127 and uses these Deform templates:
129 * ``textarea``
130 * ``readonly/notes``
131 """
132 readonly_template = 'readonly/notes'
135class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
136 """
137 Custom widget for :class:`python:set` fields.
139 This is a subclass of
140 :class:`deform:deform.widget.CheckboxChoiceWidget`.
142 :param request: Current :term:`request` object.
144 It uses these Deform templates:
146 * ``checkbox_choice``
147 * ``readonly/checkbox_choice``
148 """
150 def __init__(self, request, *args, **kwargs):
151 super().__init__(*args, **kwargs)
152 self.request = request
153 self.config = self.request.wutta_config
154 self.app = self.config.get_app()
157class WuttaDateWidget(DateInputWidget):
158 """
159 Custom widget for :class:`python:datetime.date` fields.
161 The main purpose of this widget is to leverage
162 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()`
163 for the readonly display.
165 It is automatically used for SQLAlchemy mapped classes where the
166 field maps to a :class:`sqlalchemy:sqlalchemy.types.Date` column.
167 For other (non-mapped) date fields, or mapped datetime fields for
168 which a date widget is preferred, use
169 :meth:`~wuttaweb.forms.base.Form.set_widget()`.
171 This is a subclass of
172 :class:`deform:deform.widget.DateInputWidget` and uses these
173 Deform templates:
175 * ``dateinput``
176 """
178 def __init__(self, request, *args, **kwargs):
179 super().__init__(*args, **kwargs)
180 self.request = request
181 self.config = self.request.wutta_config
182 self.app = self.config.get_app()
184 def serialize(self, field, cstruct, **kw):
185 """ """
186 readonly = kw.get('readonly', self.readonly)
187 if readonly and cstruct:
188 dt = datetime.datetime.fromisoformat(cstruct)
189 return self.app.render_date(dt)
191 return super().serialize(field, cstruct, **kw)
194class WuttaDateTimeWidget(DateTimeInputWidget):
195 """
196 Custom widget for :class:`python:datetime.datetime` fields.
198 The main purpose of this widget is to leverage
199 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()`
200 for the readonly display.
202 It is automatically used for SQLAlchemy mapped classes where the
203 field maps to a :class:`sqlalchemy:sqlalchemy.types.DateTime`
204 column. For other (non-mapped) datetime fields, you may have to
205 use it explicitly via
206 :meth:`~wuttaweb.forms.base.Form.set_widget()`.
208 This is a subclass of
209 :class:`deform:deform.widget.DateTimeInputWidget` and uses these
210 Deform templates:
212 * ``datetimeinput``
213 """
215 def __init__(self, request, *args, **kwargs):
216 super().__init__(*args, **kwargs)
217 self.request = request
218 self.config = self.request.wutta_config
219 self.app = self.config.get_app()
221 def serialize(self, field, cstruct, **kw):
222 """ """
223 readonly = kw.get('readonly', self.readonly)
224 if readonly and cstruct:
225 dt = datetime.datetime.fromisoformat(cstruct)
226 return self.app.render_datetime(dt)
228 return super().serialize(field, cstruct, **kw)
231class WuttaMoneyInputWidget(MoneyInputWidget):
232 """
233 Custom widget for "money" fields. This is used by default for
234 :class:`~wuttaweb.forms.schema.WuttaMoney` type nodes.
236 The main purpose of this widget is to leverage
237 :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()`
238 for the readonly display.
240 This is a subclass of
241 :class:`deform:deform.widget.MoneyInputWidget` and uses these
242 Deform templates:
244 * ``moneyinput``
246 :param request: Current :term:`request` object.
248 :param scale: If this kwarg is specified, it will be passed along
249 to ``render_currency()`` call.
250 """
252 def __init__(self, request, *args, **kwargs):
253 self.scale = kwargs.pop('scale', 2)
254 super().__init__(*args, **kwargs)
255 self.request = request
256 self.config = self.request.wutta_config
257 self.app = self.config.get_app()
259 def serialize(self, field, cstruct, **kw):
260 """ """
261 readonly = kw.get('readonly', self.readonly)
262 if readonly:
263 if cstruct in (colander.null, None):
264 return HTML.tag('span')
265 cstruct = decimal.Decimal(cstruct)
266 text = self.app.render_currency(cstruct, scale=self.scale)
267 return HTML.tag('span', c=[text])
269 return super().serialize(field, cstruct, **kw)
272class FileDownloadWidget(Widget):
273 """
274 Widget for use with :class:`~wuttaweb.forms.schema.FileDownload`
275 fields.
277 This only supports readonly, and shows a hyperlink to download the
278 file. Link text is the filename plus file size.
280 This is a subclass of :class:`deform:deform.widget.Widget` and
281 uses these Deform templates:
283 * ``readonly/filedownload``
285 :param request: Current :term:`request` object.
287 :param url: Optional URL for hyperlink. If not specified, file
288 name/size is shown with no hyperlink.
289 """
290 readonly_template = 'readonly/filedownload'
292 def __init__(self, request, *args, **kwargs):
293 self.url = kwargs.pop('url', None)
294 super().__init__(*args, **kwargs)
295 self.request = request
296 self.config = self.request.wutta_config
297 self.app = self.config.get_app()
299 def serialize(self, field, cstruct, **kw):
300 """ """
301 # nb. readonly is the only way this rolls
302 kw['readonly'] = True
303 template = self.readonly_template
305 path = cstruct or None
306 if path:
307 kw.setdefault('filename', os.path.basename(path))
308 kw.setdefault('filesize', self.readable_size(path))
309 if self.url:
310 kw.setdefault('url', self.url)
312 else:
313 kw.setdefault('filename', None)
314 kw.setdefault('filesize', None)
316 kw.setdefault('url', None)
317 values = self.get_template_values(field, cstruct, kw)
318 return field.renderer(template, **values)
320 def readable_size(self, path):
321 """ """
322 try:
323 size = os.path.getsize(path)
324 except os.error:
325 size = 0
326 return humanize.naturalsize(size)
329class GridWidget(Widget):
330 """
331 Widget for fields whose data is represented by a :term:`grid`.
333 This is a subclass of :class:`deform:deform.widget.Widget` but
334 does not use any Deform templates.
336 This widget only supports "readonly" mode, is not editable. It is
337 merely a convenience around the grid itself, which does the heavy
338 lifting.
340 Instead of creating this widget directly you probably should call
341 :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form.
343 :param request: Current :term:`request` object.
345 :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to
346 display the field data.
347 """
349 def __init__(self, request, grid, *args, **kwargs):
350 super().__init__(*args, **kwargs)
351 self.request = request
352 self.grid = grid
354 def serialize(self, field, cstruct, **kw):
355 """
356 This widget simply calls
357 :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on
358 the ``grid`` to serialize.
359 """
360 readonly = kw.get('readonly', self.readonly)
361 if not readonly:
362 raise NotImplementedError("edit not allowed for this widget")
364 return self.grid.render_table_element()
367class RoleRefsWidget(WuttaCheckboxChoiceWidget):
368 """
369 Widget for use with User
370 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
371 This is the default widget for the
372 :class:`~wuttaweb.forms.schema.RoleRefs` type.
374 This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
375 """
376 readonly_template = 'readonly/rolerefs'
378 def serialize(self, field, cstruct, **kw):
379 """ """
380 model = self.app.model
382 # special logic when field is editable
383 readonly = kw.get('readonly', self.readonly)
384 if not readonly:
386 # but does not apply if current user is root
387 if not self.request.is_root:
388 auth = self.app.get_auth_handler()
389 admin = auth.get_role_administrator(self.session)
391 # prune admin role from values list; it should not be
392 # one of the options since current user is not admin
393 values = kw.get('values', self.values)
394 values = [val for val in values
395 if val[0] != admin.uuid]
396 kw['values'] = values
398 else: # readonly
400 # roles
401 roles = []
402 if cstruct:
403 for uuid in cstruct:
404 role = self.session.get(model.Role, uuid)
405 if role:
406 roles.append(role)
407 kw['roles'] = roles
409 # url
410 url = lambda role: self.request.route_url('roles.view', uuid=role.uuid)
411 kw['url'] = url
413 # default logic from here
414 return super().serialize(field, cstruct, **kw)
417class UserRefsWidget(WuttaCheckboxChoiceWidget):
418 """
419 Widget for use with Role
420 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.users` field.
421 This is the default widget for the
422 :class:`~wuttaweb.forms.schema.UserRefs` type.
424 This is a subclass of :class:`WuttaCheckboxChoiceWidget`; however
425 it only supports readonly mode and does not use a template.
426 Rather, it generates and renders a
427 :class:`~wuttaweb.grids.base.Grid` showing the users list.
428 """
430 def serialize(self, field, cstruct, **kw):
431 """ """
432 readonly = kw.get('readonly', self.readonly)
433 if not readonly:
434 raise NotImplementedError("edit not allowed for this widget")
436 model = self.app.model
437 columns = ['username', 'active']
439 # generate data set for users
440 users = []
441 if cstruct:
442 for uuid in cstruct:
443 user = self.session.get(model.User, uuid)
444 if user:
445 users.append(dict([(key, getattr(user, key))
446 for key in columns + ['uuid']]))
448 # do not render if no data
449 if not users:
450 return HTML.tag('span')
452 # grid
453 grid = Grid(self.request, key='roles.view.users',
454 columns=columns, data=users)
456 # view action
457 if self.request.has_perm('users.view'):
458 url = lambda user, i: self.request.route_url('users.view', uuid=user['uuid'])
459 grid.add_action('view', icon='eye', url=url)
460 grid.set_link('person')
461 grid.set_link('username')
463 # edit action
464 if self.request.has_perm('users.edit'):
465 url = lambda user, i: self.request.route_url('users.edit', uuid=user['uuid'])
466 grid.add_action('edit', url=url)
468 # render as simple <b-table>
469 # nb. must indicate we are a part of this form
470 form = getattr(field.parent, 'wutta_form', None)
471 return grid.render_table_element(form)
474class PermissionsWidget(WuttaCheckboxChoiceWidget):
475 """
476 Widget for use with Role
477 :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
478 field.
480 This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses
481 these Deform templates:
483 * ``permissions``
484 * ``readonly/permissions``
485 """
486 template = 'permissions'
487 readonly_template = 'readonly/permissions'
489 def serialize(self, field, cstruct, **kw):
490 """ """
491 kw.setdefault('permissions', self.permissions)
493 if 'values' not in kw:
494 values = []
495 for gkey, group in self.permissions.items():
496 for pkey, perm in group['perms'].items():
497 values.append((pkey, perm['label']))
498 kw['values'] = values
500 return super().serialize(field, cstruct, **kw)
503class EmailRecipientsWidget(TextAreaWidget):
504 """
505 Widget for :term:`email setting` recipient fields (``To``, ``Cc``,
506 ``Bcc``).
508 This is a subclass of
509 :class:`deform:deform.widget.TextAreaWidget`. It uses these
510 Deform templates:
512 * ``textarea``
513 * ``readonly/email_recips``
515 See also the :class:`~wuttaweb.forms.schema.EmailRecipients`
516 schema type, which uses this widget.
517 """
518 readonly_template = 'readonly/email_recips'
520 def serialize(self, field, cstruct, **kw):
521 """ """
522 readonly = kw.get('readonly', self.readonly)
523 if readonly:
524 kw['recips'] = parse_list(cstruct or '')
526 return super().serialize(field, cstruct, **kw)
528 def deserialize(self, field, pstruct):
529 """ """
530 if pstruct is colander.null:
531 return colander.null
533 values = [value for value in parse_list(pstruct)
534 if value]
535 return ', '.join(values)
538class BatchIdWidget(Widget):
539 """
540 Widget for use with the
541 :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id`
542 field of a :term:`batch` model.
544 This widget is "always" read-only and renders the Batch ID as
545 zero-padded 8-char string
546 """
548 def serialize(self, field, cstruct, **kw):
549 """ """
550 if cstruct is colander.null:
551 return colander.null
553 batch_id = int(cstruct)
554 return f'{batch_id:08d}'