diff --git a/CHANGELOG.md b/CHANGELOG.md index 9849d37..899001e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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.28.2 (2026-02-25) + +### Fix + +- only show string filter input if data type matches +- move `coerce_value()` method to base `GridFilter` class +- preserve original data type when un-setting filter choices +- raise better error if field widget serialization fails +- tweak how grid filter params are built, for better link share +- allow passing filter factory to `Grid.set_filter()` +- track column-only labels separately for grid +- make view responsible for App Info field/values + ## v0.28.1 (2026-02-17) ### Fix diff --git a/pyproject.toml b/pyproject.toml index ad847e2..5b7eb80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.28.1" +version = "0.28.2" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index b1e142a..796fd6f 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -1138,7 +1138,18 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth field = dform[fieldname] if readonly: kwargs["readonly"] = True - html = field.serialize(**kwargs) + + try: + html = field.serialize(**kwargs) + except Exception as exc: + log.warning( + "widget serialization failed for field: %s", + fieldname, + exc_info=True, + ) + raise RuntimeError( + f"widget serialization failed for field: {fieldname}" + ) from exc else: # render static text if field not in deform/schema diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 077f51a..2ac2646 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -119,9 +119,17 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth .. attribute:: labels - Dict of column label overrides. + Dict of column and/or filter label overrides. - See also :meth:`get_label()` and :meth:`set_label()`. + See also :attr:`column_labels`, :meth:`set_label()`, + :meth:`get_column_label()` and :meth:`get_filter_label()`. + + .. attribute:: column_labels + + Dict of label overrides for column only. + + See also :attr:`labels`, :meth:`set_label()` and + :meth:`get_column_label()`. .. attribute:: centered @@ -434,6 +442,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth self.key = key self.data = data self.labels = labels or {} + self.column_labels = {} self.checkable = checkable self.row_class = row_class self.actions = actions or [] @@ -628,35 +637,61 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth def set_label(self, key, label, column_only=False): """ - Set/override the label for a column. + Set/override the label for a column and/or filter. - :param key: Name of column. + :param key: Key for the column/filter. - :param label: New label for the column header. + :param label: New label for the column and/or filter. :param column_only: Boolean indicating whether the label should be applied *only* to the column header (if ``True``), vs. applying also to the filter (if ``False``). - See also :meth:`get_label()`. Label overrides are tracked via - :attr:`labels`. + See also :meth:`get_column_label()` and + :meth:`get_filter_label()`. Label overrides are tracked via + :attr:`labels` and :attr:`column_labels`. """ - self.labels[key] = label + if column_only: + self.column_labels[key] = label + else: + self.labels[key] = label + if key in self.filters: + self.filters[key].label = label - if not column_only and key in self.filters: - self.filters[key].label = label + def get_label(self, key): # pylint: disable=missing-function-docstring + warnings.warn( + "Grid.get_label() is deprecated; please use " + "get_filter_label() or get_column_label() instead", + DeprecationWarning, + stacklevel=2, + ) + return self.get_filter_label(key) - def get_label(self, key): + def get_filter_label(self, key): + """ + Returns the label text for a given filter. + + If no override is defined, the label is derived from ``key``. + + See also :meth:`set_label()` and :meth:`get_column_label()`. + """ + if key in self.labels: + return self.labels[key] + + return self.app.make_title(key) + + def get_column_label(self, key): """ Returns the label text for a given column. If no override is defined, the label is derived from ``key``. - See also :meth:`set_label()`. + See also :meth:`set_label()` and :meth:`get_filter_label()`. """ - if key in self.labels: - return self.labels[key] - return self.app.make_title(key) + if key in self.column_labels: + return self.column_labels[key] + + return self.get_filter_label(key) def set_centered(self, key, centered=True): """ @@ -1369,10 +1404,11 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth Code usually does not need to call this directly. See also :meth:`set_filter()`, which calls this method automatically. - :param columninfo: Can be either a model property (see below), - or a column name. + :param columninfo: Can be either a model property + (e.g. ``model.User.username``), or a column name + (e.g. ``"username"``). - :returns: A :class:`~wuttaweb.grids.filters.GridFilter` + :returns: :class:`~wuttaweb.grids.filters.GridFilter` instance. """ key = kwargs.pop("key", None) @@ -1410,12 +1446,18 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth :param key: Name of column. - :param filterinfo: Can be either a - :class:`~wuttweb.grids.filters.GridFilter` instance, or - else a model property (see below). + :param filterinfo: Can be either a filter factory, or else a + model property (e.g. ``model.User.username``) or column + name (e.g. ``"username"``). If not specified then the + ``key`` will be used instead. - If ``filterinfo`` is a ``GridFilter`` instance, it will be - used as-is for the backend filter. + :param \\**kwargs: Additional kwargs to pass along to the + filter factory. + + If ``filterinfo`` is a factory, it will be called with the + current request, key and kwargs like so:: + + filtr = factory(self.request, key, **kwargs) Otherwise :meth:`make_filter()` will be called to obtain the backend filter. The ``filterinfo`` will be passed along to @@ -1427,12 +1469,13 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth filtr = None if filterinfo and callable(filterinfo): - # filtr = filterinfo - raise NotImplementedError + kwargs.setdefault("label", self.get_filter_label(key)) + filtr = filterinfo(self.request, key, **kwargs) - kwargs["key"] = key - kwargs.setdefault("label", self.get_label(key)) - filtr = self.make_filter(filterinfo or key, **kwargs) + else: + kwargs["key"] = key + kwargs.setdefault("label", self.get_filter_label(key)) + filtr = self.make_filter(filterinfo or key, **kwargs) self.filters[key] = filtr @@ -2314,7 +2357,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth columns.append( { "field": name, - "label": self.get_label(name), + "label": self.get_column_label(name), "hidden": self.is_hidden(name), "sortable": self.is_sortable(name), "searchable": self.is_searchable(name), diff --git a/src/wuttaweb/grids/filters.py b/src/wuttaweb/grids/filters.py index 170fbd9..f34d848 100644 --- a/src/wuttaweb/grids/filters.py +++ b/src/wuttaweb/grids/filters.py @@ -189,6 +189,10 @@ class GridFilter: # pylint: disable=too-many-instance-attributes self.app = self.config.get_app() self.label = label or self.app.make_title(self.key) + # remember original data type in case we need to revert, + # e.g. after changing it to 'choices' and back again + self.original_data_type = self.data_type + # active self.default_active = default_active self.active = self.default_active @@ -201,7 +205,7 @@ class GridFilter: # pylint: disable=too-many-instance-attributes self.verb = None # active verb is set later # choices - self.set_choices(choices or {}) + self.set_choices(choices) # nullable self.nullable = nullable @@ -304,7 +308,8 @@ class GridFilter: # pylint: disable=too-many-instance-attributes self.data_type = "choice" else: self.choices = {} - self.data_type = "string" + if self.data_type == "choice": + self.data_type = self.original_data_type def normalize_choices(self, choices): """ @@ -345,6 +350,26 @@ class GridFilter: # pylint: disable=too-many-instance-attributes return normalized + def coerce_value(self, value): + """ + Coerce the given value to the correct type/format for use with + the filter. This is where e.g. a boolean or date filter + should convert input string to ``bool`` or ``date`` value. + + This is (usually) called from a filter method, when applying + the filter. See also :meth:`apply_filter()`. + + Default logic on the base class returns value as-is; subclass + may override as needed. + + :param value: Input string provided by the user via the filter + form submission. + + :returns: Value of the appropriate type, depending on the + filter subclass. + """ + return value + def apply_filter(self, data, verb=None, value=UNSPECIFIED): """ Filter the given data set according to a verb/value pair. @@ -415,15 +440,6 @@ class AlchemyFilter(GridFilter): if len(columns) == 1: self.nullable = columns[0].nullable - def coerce_value(self, value): - """ - Coerce the given value to the correct type/format for use with - the filter. - - Default logic returns value as-is; subclass may override. - """ - return value - def filter_equal(self, query, value): """ Filter data with an equal (``=``) condition. diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako index d296389..b7beb8a 100644 --- a/src/wuttaweb/templates/appinfo/index.mako +++ b/src/wuttaweb/templates/appinfo/index.mako @@ -7,30 +7,11 @@
Application