diff --git a/CHANGELOG.md b/CHANGELOG.md index 646afbd..57338df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,26 +5,6 @@ All notable changes to wuttaweb will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.19.1 (2025-01-06) - -### Fix - -- improve built-in grid renderer logic -- allow session injection for ObjectRef constructor -- improve rendering for batch row status -- add basic support for row grid "view" action links -- add "xref buttons" tool panel for master view -- add WuttaQuantity schema type, widget -- remove `session` param from some form schema, widget classes -- add grid renderers for bool, currency, quantity -- use proper bulma styles for markdown content -- use span element for readonly money field widget render -- include grid filters for all column properties of model class -- use app handler to render error string, when progress fails -- add schema node type, widget for "money" (currency) fields -- exclude FK fields by default, for model forms -- fix style for header title text - ## v0.19.0 (2024-12-23) ### Feat diff --git a/pyproject.toml b/pyproject.toml index f1bb921..073ed88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.19.1" +version = "0.19.0" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -45,7 +45,7 @@ dependencies = [ "SQLAlchemy-Utils", "waitress", "WebHelpers2", - "WuttJamaican[db]>=0.19.2", + "WuttJamaican[db]>=0.19.1", "zope.sqlalchemy>=1.5", ] diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 3d0e08b..7591a7a 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -177,41 +177,25 @@ class WuttaMoney(colander.Money): return widgets.WuttaMoneyInputWidget(self.request, **kwargs) -class WuttaQuantity(colander.Decimal): - """ - Custom schema type for "quantity" fields. - - This is a subclass of :class:`colander:colander.Decimal` but uses - :class:`~wuttaweb.forms.widgets.WuttaQuantityWidget` by default. - - :param request: Current :term:`request` object. - """ - - 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 widget_maker(self, **kwargs): - """ """ - return widgets.WuttaQuantityWidget(self.request, **kwargs) - - class WuttaSet(colander.Set): """ Custom schema type for :class:`python:set` fields. - This is a subclass of :class:`colander.Set`. + This is a subclass of :class:`colander.Set`, but adds + Wutta-related params to the constructor. :param request: Current :term:`request` object. + + :param session: Optional :term:`db session` to use instead of + :class:`wuttaweb.db.sess.Session`. """ - def __init__(self, request): + def __init__(self, request, session=None): super().__init__() self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() + self.session = session or Session() class ObjectRef(colander.SchemaType): @@ -247,16 +231,16 @@ class ObjectRef(colander.SchemaType): self, request, empty_option=None, + session=None, *args, **kwargs, ): - # nb. allow session injection for tests - self.session = kwargs.pop('session', Session()) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() self.model_instance = None + self.session = session or Session() if empty_option: if empty_option is True: @@ -488,7 +472,7 @@ class RoleRefs(WuttaSet): :returns: Instance of :class:`~wuttaweb.forms.widgets.RoleRefsWidget`. """ - session = kwargs.setdefault('session', Session()) + kwargs.setdefault('session', self.session) if 'values' not in kwargs: model = self.app.model @@ -496,20 +480,20 @@ class RoleRefs(WuttaSet): # avoid built-ins which cannot be assigned to users avoid = { - auth.get_role_authenticated(session), - auth.get_role_anonymous(session), + auth.get_role_authenticated(self.session), + auth.get_role_anonymous(self.session), } avoid = set([role.uuid for role in avoid]) # also avoid admin unless current user is root if not self.request.is_root: - avoid.add(auth.get_role_administrator(session).uuid) + avoid.add(auth.get_role_administrator(self.session).uuid) # everything else can be (un)assigned for users - roles = session.query(model.Role)\ - .filter(~model.Role.uuid.in_(avoid))\ - .order_by(model.Role.name)\ - .all() + roles = self.session.query(model.Role)\ + .filter(~model.Role.uuid.in_(avoid))\ + .order_by(model.Role.name)\ + .all() values = [(role.uuid.hex, role.name) for role in roles] kwargs['values'] = values @@ -534,7 +518,7 @@ class UserRefs(WuttaSet): :returns: Instance of :class:`~wuttaweb.forms.widgets.UserRefsWidget`. """ - kwargs.setdefault('session', Session()) + kwargs.setdefault('session', self.session) return widgets.UserRefsWidget(self.request, **kwargs) @@ -564,7 +548,7 @@ class Permissions(WuttaSet): :returns: Instance of :class:`~wuttaweb.forms.widgets.PermissionsWidget`. """ - kwargs.setdefault('session', Session()) + kwargs.setdefault('session', self.session) kwargs.setdefault('permissions', self.permissions) if 'values' not in kwargs: diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index a6f33d2..0af8bab 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -136,21 +136,26 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget): Custom widget for :class:`python:set` fields. This is a subclass of - :class:`deform:deform.widget.CheckboxChoiceWidget`. + :class:`deform:deform.widget.CheckboxChoiceWidget`, but adds + Wutta-related params to the constructor. :param request: Current :term:`request` object. + :param session: Optional :term:`db session` to use instead of + :class:`wuttaweb.db.sess.Session`. + It uses these Deform templates: * ``checkbox_choice`` * ``readonly/checkbox_choice`` """ - def __init__(self, request, *args, **kwargs): + def __init__(self, request, session=None, *args, **kwargs): super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config self.app = self.config.get_app() + self.session = session or Session() class WuttaDateTimeWidget(DateTimeInputWidget): @@ -226,42 +231,6 @@ class WuttaMoneyInputWidget(MoneyInputWidget): return super().serialize(field, cstruct, **kw) -class WuttaQuantityWidget(TextInputWidget): - """ - Custom widget for "quantity" fields. This is used by default for - :class:`~wuttaweb.forms.schema.WuttaQuantity` type nodes. - - The main purpose of this widget is to leverage - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()` - for the readonly display. - - This is a subclass of - :class:`deform:deform.widget.TextInputWidget` and uses these - Deform templates: - - * ``textinput`` - - :param request: Current :term:`request` object. - """ - - 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: - if cstruct in (colander.null, None): - return HTML.tag('span') - cstruct = decimal.Decimal(cstruct) - return HTML.tag('span', c=[self.app.render_quantity(cstruct)]) - - return super().serialize(field, cstruct, **kw) - - class FileDownloadWidget(Widget): """ Widget for use with :class:`~wuttaweb.forms.schema.FileDownload` diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 3a3d4f5..b9f0de7 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -116,8 +116,7 @@ class Grid: Dict of column (cell) value renderer overrides. - See also :meth:`set_renderer()` and - :meth:`set_default_renderers()`. + See also :meth:`set_renderer()`. .. attribute:: row_class @@ -389,6 +388,7 @@ class Grid: self.key = key self.data = data self.labels = labels or {} + self.renderers = renderers or {} self.row_class = row_class self.actions = actions or [] self.linked_columns = linked_columns or [] @@ -398,10 +398,6 @@ class Grid: self.app = self.config.get_app() self.set_columns(columns or self.get_columns()) - self.renderers = {} - if renderers: - for key, val in renderers.items(): - self.set_renderer(key, val) self.set_default_renderers() self.set_tools(tools) @@ -596,29 +592,8 @@ class Grid: grid = Grid(request, columns=['foo', 'bar']) grid.set_renderer('foo', render_foo) - For convenience, in lieu of a renderer callable, you may - specify one of the following strings, which will be - interpreted as a built-in renderer callable, as shown below: - - * ``'batch_id'`` -> :meth:`render_batch_id()` - * ``'boolean'`` -> :meth:`render_boolean()` - * ``'currency'`` -> :meth:`render_currency()` - * ``'datetime'`` -> :meth:`render_datetime()` - * ``'quantity'`` -> :meth:`render_quantity()` - Renderer overrides are tracked via :attr:`renderers`. """ - builtins = { - 'batch_id': self.render_batch_id, - 'boolean': self.render_boolean, - 'currency': self.render_currency, - 'datetime': self.render_datetime, - 'quantity': self.render_quantity, - } - - if renderer in builtins: - renderer = builtins[renderer] - if kwargs: renderer = functools.partial(renderer, **kwargs) self.renderers[key] = renderer @@ -627,18 +602,15 @@ class Grid: """ Set default column value renderers, where applicable. - This is called automatically from the class constructor. It - will add new entries to :attr:`renderers` for columns whose - data type implies a default renderer. This is only possible - if :attr:`model_class` is set to a SQLAlchemy mapped class. + This will add new entries to :attr:`renderers` for columns + whose data type implies a default renderer should be used. + This is generally only possible if :attr:`model_class` is set + to a valid SQLAlchemy mapped class. - This only looks for a couple of data types, and configures as - follows: - - * :class:`sqlalchemy:sqlalchemy.types.Boolean` -> - :meth:`render_boolean()` - * :class:`sqlalchemy:sqlalchemy.types.DateTime` -> - :meth:`render_datetime()` + This (for now?) only looks for + :class:`sqlalchemy:sqlalchemy.types.DateTime` columns and if + any are found, they are configured to use + :meth:`render_datetime()`. """ if not self.model_class: return @@ -654,8 +626,6 @@ class Grid: column = prop.columns[0] if isinstance(column.type, sa.DateTime): self.set_renderer(key, self.render_datetime) - elif isinstance(column.type, sa.Boolean): - self.set_renderer(key, self.render_boolean) def set_link(self, key, link=True): """ @@ -1783,80 +1753,23 @@ class Grid: # rendering methods ############################## - def render_batch_id(self, obj, key, value): - """ - Column renderer for batch ID values. - - This is not used automatically but you can use it explicitly:: - - grid.set_renderer('foo', 'batch_id') - """ - if value is None: - return "" - - batch_id = int(value) - return f'{batch_id:08d}' - - def render_boolean(self, obj, key, value): - """ - Column renderer for boolean values. - - This calls - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_boolean()` - for the return value. - - This may be used automatically per - :meth:`set_default_renderers()` or you can use it explicitly:: - - grid.set_renderer('foo', grid.render_boolean) - """ - return self.app.render_boolean(value) - - def render_currency(self, obj, key, value, **kwargs): - """ - Column renderer for currency values. - - This calls - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()` - for the return value. - - This is not used automatically but you can use it explicitly:: - - grid.set_renderer('foo', 'currency') - grid.set_renderer('foo', 'currency', scale=4) - """ - return self.app.render_currency(value, **kwargs) - def render_datetime(self, obj, key, value): """ - Column renderer for :class:`python:datetime.datetime` values. - - This calls + Default cell value renderer for + :class:`sqlalchemy:sqlalchemy.types.DateTime` columns, which + calls :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()` for the return value. This may be used automatically per - :meth:`set_default_renderers()` or you can use it explicitly:: + :meth:`set_default_renderers()` or you can use it explicitly + for any :class:`python:datetime.datetime` column with:: grid.set_renderer('foo', grid.render_datetime) """ dt = getattr(obj, key) return self.app.render_datetime(dt) - def render_quantity(self, obj, key, value): - """ - Column renderer for quantity values. - - This calls - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()` - for the return value. - - This is not used automatically but you can use it explicitly:: - - grid.set_renderer('foo', grid.render_quantity) - """ - return self.app.render_quantity(value) - def render_table_element( self, form=None, diff --git a/src/wuttaweb/templates/batch/view.mako b/src/wuttaweb/templates/batch/view.mako index 1305079..f03ab5b 100644 --- a/src/wuttaweb/templates/batch/view.mako +++ b/src/wuttaweb/templates/batch/view.mako @@ -1,6 +1,19 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> +<%def name="extra_styles()"> + ${parent.extra_styles()} + +%def> + <%def name="tool_panels()"> ${parent.tool_panels()} ${self.tool_panel_execution()} @@ -52,7 +65,7 @@
What will happen when this batch is executed?
-