Source code for wuttaweb.forms.widgets

# -*- coding: utf-8; -*-
################################################################################
#
#  wuttaweb -- Web App for Wutta Framework
#  Copyright © 2024 Lance Edgar
#
#  This file is part of Wutta Framework.
#
#  Wutta Framework is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  Wutta Framework is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Form widgets

This module defines some custom widgets for use with WuttaWeb.

However for convenience it also makes other Deform widgets available
in the namespace:

* :class:`deform:deform.widget.Widget` (base class)
* :class:`deform:deform.widget.TextInputWidget`
* :class:`deform:deform.widget.TextAreaWidget`
* :class:`deform:deform.widget.PasswordWidget`
* :class:`deform:deform.widget.CheckedPasswordWidget`
* :class:`deform:deform.widget.CheckboxWidget`
* :class:`deform:deform.widget.SelectWidget`
* :class:`deform:deform.widget.CheckboxChoiceWidget`
* :class:`deform:deform.widget.DateInputWidget`
* :class:`deform:deform.widget.DateTimeInputWidget`
* :class:`deform:deform.widget.MoneyInputWidget`
"""

import datetime
import decimal
import os

import colander
import humanize
from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
                           PasswordWidget, CheckedPasswordWidget,
                           CheckboxWidget, SelectWidget, CheckboxChoiceWidget,
                           DateInputWidget, DateTimeInputWidget, MoneyInputWidget)
from webhelpers2.html import HTML

from wuttjamaican.conf import parse_list

from wuttaweb.db import Session


[docs] class ObjectRefWidget(SelectWidget): """ Widget for use with model "object reference" fields, e.g. foreign key UUID => TargetModel instance. While you may create instances of this widget directly, it normally happens automatically when schema nodes of the :class:`~wuttaweb.forms.schema.ObjectRef` (sub)type are part of the form schema; via :meth:`~wuttaweb.forms.schema.ObjectRef.widget_maker()`. In readonly mode, this renders a ``<span>`` tag around the :attr:`model_instance` (converted to string). Otherwise it renders a select (dropdown) element allowing user to choose from available records. This is a subclass of :class:`deform:deform.widget.SelectWidget` and uses these Deform templates: * ``select`` * ``readonly/objectref`` .. attribute:: model_instance Reference to the model record instance, i.e. the "far side" of the foreign key relationship. .. note:: You do not need to provide the ``model_instance`` when constructing the widget. Rather, it is set automatically when the :class:`~wuttaweb.forms.schema.ObjectRef` type instance (associated with the node) is serialized. """ readonly_template = 'readonly/objectref' def __init__(self, request, url=None, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.url = url def get_template_values(self, field, cstruct, kw): """ """ values = super().get_template_values(field, cstruct, kw) # add url, only if rendering readonly readonly = kw.get('readonly', self.readonly) if readonly: if 'url' not in values and self.url and getattr(field.schema, 'model_instance', None): values['url'] = self.url(field.schema.model_instance) return values
[docs] class NotesWidget(TextAreaWidget): """ Widget for use with "notes" fields. In readonly mode, this shows the notes with a background to make them stand out a bit more. Otherwise it effectively shows a ``<textarea>`` input element. This is a subclass of :class:`deform:deform.widget.TextAreaWidget` and uses these Deform templates: * ``textarea`` * ``readonly/notes`` """ readonly_template = 'readonly/notes'
[docs] class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): """ Custom widget for :class:`python:set` fields. This is a subclass of :class:`deform:deform.widget.CheckboxChoiceWidget`. :param request: Current :term:`request` object. It uses these Deform templates: * ``checkbox_choice`` * ``readonly/checkbox_choice`` """ def __init__(self, request, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app()
[docs] class WuttaDateWidget(DateInputWidget): """ Custom widget for :class:`python:datetime.date` fields. The main purpose of this widget is to leverage :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()` for the readonly display. It is automatically used for SQLAlchemy mapped classes where the field maps to a :class:`sqlalchemy:sqlalchemy.types.Date` column. For other (non-mapped) date fields, or mapped datetime fields for which a date widget is preferred, use :meth:`~wuttaweb.forms.base.Form.set_widget()`. This is a subclass of :class:`deform:deform.widget.DateInputWidget` and uses these Deform templates: * ``dateinput`` """ def __init__(self, request, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() def serialize(self, field, cstruct, **kw): """ """ readonly = kw.get('readonly', self.readonly) if readonly and cstruct: dt = datetime.datetime.fromisoformat(cstruct) return self.app.render_date(dt) return super().serialize(field, cstruct, **kw)
[docs] class WuttaDateTimeWidget(DateTimeInputWidget): """ Custom widget for :class:`python:datetime.datetime` fields. The main purpose of this widget is to leverage :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()` for the readonly display. It is automatically used for SQLAlchemy mapped classes where the field maps to a :class:`sqlalchemy:sqlalchemy.types.DateTime` column. For other (non-mapped) datetime fields, you may have to use it explicitly via :meth:`~wuttaweb.forms.base.Form.set_widget()`. This is a subclass of :class:`deform:deform.widget.DateTimeInputWidget` and uses these Deform templates: * ``datetimeinput`` """ def __init__(self, request, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() def serialize(self, field, cstruct, **kw): """ """ readonly = kw.get('readonly', self.readonly) if readonly and cstruct: dt = datetime.datetime.fromisoformat(cstruct) return self.app.render_datetime(dt) return super().serialize(field, cstruct, **kw)
[docs] class WuttaMoneyInputWidget(MoneyInputWidget): """ Custom widget for "money" fields. This is used by default for :class:`~wuttaweb.forms.schema.WuttaMoney` type nodes. The main purpose of this widget is to leverage :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()` for the readonly display. This is a subclass of :class:`deform:deform.widget.MoneyInputWidget` and uses these Deform templates: * ``moneyinput`` :param request: Current :term:`request` object. :param scale: If this kwarg is specified, it will be passed along to ``render_currency()`` call. """ def __init__(self, request, *args, **kwargs): self.scale = kwargs.pop('scale', 2) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() def serialize(self, field, cstruct, **kw): """ """ readonly = kw.get('readonly', self.readonly) if readonly: if cstruct in (colander.null, None): return HTML.tag('span') cstruct = decimal.Decimal(cstruct) text = self.app.render_currency(cstruct, scale=self.scale) return HTML.tag('span', c=[text]) return super().serialize(field, cstruct, **kw)
[docs] class FileDownloadWidget(Widget): """ Widget for use with :class:`~wuttaweb.forms.schema.FileDownload` fields. This only supports readonly, and shows a hyperlink to download the file. Link text is the filename plus file size. This is a subclass of :class:`deform:deform.widget.Widget` and uses these Deform templates: * ``readonly/filedownload`` :param request: Current :term:`request` object. :param url: Optional URL for hyperlink. If not specified, file name/size is shown with no hyperlink. """ readonly_template = 'readonly/filedownload' def __init__(self, request, *args, **kwargs): self.url = kwargs.pop('url', None) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() def serialize(self, field, cstruct, **kw): """ """ # nb. readonly is the only way this rolls kw['readonly'] = True template = self.readonly_template path = cstruct or None if path: kw.setdefault('filename', os.path.basename(path)) kw.setdefault('filesize', self.readable_size(path)) if self.url: kw.setdefault('url', self.url) else: kw.setdefault('filename', None) kw.setdefault('filesize', None) kw.setdefault('url', None) values = self.get_template_values(field, cstruct, kw) return field.renderer(template, **values) def readable_size(self, path): """ """ try: size = os.path.getsize(path) except os.error: size = 0 return humanize.naturalsize(size)
[docs] class GridWidget(Widget): """ Widget for fields whose data is represented by a :term:`grid`. This is a subclass of :class:`deform:deform.widget.Widget` but does not use any Deform templates. This widget only supports "readonly" mode, is not editable. It is merely a convenience around the grid itself, which does the heavy lifting. Instead of creating this widget directly you probably should call :meth:`~wuttaweb.forms.base.Form.set_grid()` on your form. :param request: Current :term:`request` object. :param grid: :class:`~wuttaweb.grids.base.Grid` instance, used to display the field data. """ def __init__(self, request, grid, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.grid = grid
[docs] def serialize(self, field, cstruct, **kw): """ This widget simply calls :meth:`~wuttaweb.grids.base.Grid.render_table_element()` on the ``grid`` to serialize. """ readonly = kw.get('readonly', self.readonly) if not readonly: raise NotImplementedError("edit not allowed for this widget") return self.grid.render_table_element()
[docs] class RoleRefsWidget(WuttaCheckboxChoiceWidget): """ Widget for use with User :attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field. This is the default widget for the :class:`~wuttaweb.forms.schema.RoleRefs` type. This is a subclass of :class:`WuttaCheckboxChoiceWidget`. """ readonly_template = 'readonly/rolerefs' def serialize(self, field, cstruct, **kw): """ """ model = self.app.model # special logic when field is editable readonly = kw.get('readonly', self.readonly) if not readonly: # but does not apply if current user is root if not self.request.is_root: auth = self.app.get_auth_handler() admin = auth.get_role_administrator(self.session) # prune admin role from values list; it should not be # one of the options since current user is not admin values = kw.get('values', self.values) values = [val for val in values if val[0] != admin.uuid] kw['values'] = values else: # readonly # roles roles = [] if cstruct: for uuid in cstruct: role = self.session.get(model.Role, uuid) if role: roles.append(role) kw['roles'] = roles # url url = lambda role: self.request.route_url('roles.view', uuid=role.uuid) kw['url'] = url # default logic from here return super().serialize(field, cstruct, **kw)
[docs] class PermissionsWidget(WuttaCheckboxChoiceWidget): """ Widget for use with Role :attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions` field. This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses these Deform templates: * ``permissions`` * ``readonly/permissions`` """ template = 'permissions' readonly_template = 'readonly/permissions' def serialize(self, field, cstruct, **kw): """ """ kw.setdefault('permissions', self.permissions) if 'values' not in kw: values = [] for gkey, group in self.permissions.items(): for pkey, perm in group['perms'].items(): values.append((pkey, perm['label'])) kw['values'] = values return super().serialize(field, cstruct, **kw)
[docs] class EmailRecipientsWidget(TextAreaWidget): """ Widget for :term:`email setting` recipient fields (``To``, ``Cc``, ``Bcc``). This is a subclass of :class:`deform:deform.widget.TextAreaWidget`. It uses these Deform templates: * ``textarea`` * ``readonly/email_recips`` See also the :class:`~wuttaweb.forms.schema.EmailRecipients` schema type, which uses this widget. """ readonly_template = 'readonly/email_recips' def serialize(self, field, cstruct, **kw): """ """ readonly = kw.get('readonly', self.readonly) if readonly: kw['recips'] = parse_list(cstruct or '') return super().serialize(field, cstruct, **kw) def deserialize(self, field, pstruct): """ """ if pstruct is colander.null: return colander.null values = [value for value in parse_list(pstruct) if value] return ', '.join(values)
[docs] class BatchIdWidget(Widget): """ Widget for use with the :attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.id` field of a :term:`batch` model. This widget is "always" read-only and renders the Batch ID as zero-padded 8-char string """ def serialize(self, field, cstruct, **kw): """ """ if cstruct is colander.null: return colander.null batch_id = int(cstruct) return f'{batch_id:08d}'