diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index a754a2c..fcfff93 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -119,6 +119,12 @@ class Grid:
See also :meth:`set_renderer()` and
:meth:`set_default_renderers()`.
+ .. attribute:: enums
+
+ Dict of "enum" collections, for supported columns.
+
+ See also :meth:`set_enum()`.
+
.. attribute:: checkable
Boolean indicating whether the grid should expose per-row
@@ -377,6 +383,7 @@ class Grid:
data=None,
labels={},
renderers={},
+ enums={},
checkable=False,
row_class=None,
actions=[],
@@ -458,6 +465,11 @@ class Grid:
self.filters = {}
self.set_filter_defaults(**(filter_defaults or {}))
+ # enums
+ self.enums = {}
+ for key in enums:
+ self.set_enum(key, enums[key])
+
def get_columns(self):
"""
Returns the official list of column names for the grid, or
@@ -720,6 +732,27 @@ class Grid:
elif isinstance(column.type, sa.Boolean):
self.set_renderer(key, self.render_boolean)
+ def set_enum(self, key, enum):
+ """
+ Set the "enum" collection for a given column.
+
+ This will set the column renderer to show the appropriate enum
+ value for each row in the grid. See also
+ :meth:`render_enum()`.
+
+ If the grid has a corresponding filter for the column, it will
+ be modified to show "choices" for values contained in the
+ enum.
+
+ :param key: Name of column.
+
+ :param enum: Instance of :class:`python:enum.Enum`.
+ """
+ self.enums[key] = enum
+ self.set_renderer(key, self.render_enum, enum=enum)
+ if key in self.filters:
+ self.filters[key].set_choices(enum)
+
def set_link(self, key, link=True):
"""
Explicitly enable or disable auto-link behavior for a given
@@ -1945,6 +1978,33 @@ class Grid:
dt = getattr(obj, key)
return self.app.render_datetime(dt)
+ def render_enum(self, obj, key, value, enum=None):
+ """
+ Custom grid value renderer for "enum" fields.
+
+ See also :meth:`set_enum()`.
+
+ :param enum: Enum class for the field. This should be an
+ instance of :class:`~python:enum.Enum`.
+
+ To use this feature for your grid::
+
+ from enum import Enum
+
+ class MyEnum(Enum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+ grid.set_enum('my_enum_field', MyEnum)
+ """
+ if enum:
+ raw_value = obj[key]
+ if raw_value:
+ return raw_value.value
+
+ return value
+
def render_percent(self, obj, key, value, **kwargs):
"""
Column renderer for percentage values.
@@ -2176,6 +2236,13 @@ class Grid:
"""
filters = []
for filtr in self.filters.values():
+
+ choices = []
+ choice_labels = {}
+ if filtr.choices:
+ choices = list(filtr.choices)
+ choice_labels = dict(filtr.choices)
+
filters.append({
'key': filtr.key,
'data_type': filtr.data_type,
@@ -2185,6 +2252,8 @@ class Grid:
'verb_labels': filtr.get_verb_labels(),
'valueless_verbs': filtr.get_valueless_verbs(),
'verb': filtr.verb,
+ 'choices': choices,
+ 'choice_labels': choice_labels,
'value': filtr.value,
'label': filtr.label,
})
diff --git a/src/wuttaweb/grids/filters.py b/src/wuttaweb/grids/filters.py
index 1250b3d..1ecee1c 100644
--- a/src/wuttaweb/grids/filters.py
+++ b/src/wuttaweb/grids/filters.py
@@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
-# Copyright © 2024 Lance Edgar
+# Copyright © 2024-2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
@@ -26,6 +26,8 @@ Grid Filters
import datetime
import logging
+from collections import OrderedDict
+from enum import EnumType
import sqlalchemy as sa
@@ -77,6 +79,7 @@ class GridFilter:
* ``'string'``
* ``'date'``
+ * ``'choice'``
Note that this mainly applies to the "value input" used by the
filter. There is no data type for boolean since it does not
@@ -94,6 +97,13 @@ class GridFilter:
See also :attr:`value`.
+ .. attribute:: choices
+
+ OrderedDict of possible values for the filter.
+
+ This is safe to read from, but use :meth:`set_choices()` to
+ update it.
+
.. attribute:: value
Value for current filter, if :attr:`active` is true.
@@ -159,6 +169,7 @@ class GridFilter:
key,
label=None,
verbs=None,
+ choices={},
default_active=False,
default_verb=None,
default_value=None,
@@ -180,6 +191,9 @@ class GridFilter:
if default_verb:
self.default_verb = default_verb
+ # choices
+ self.set_choices(choices)
+
# value
self.default_value = default_value
self.value = self.default_value
@@ -255,6 +269,72 @@ class GridFilter:
return verb
+ def set_choices(self, choices):
+ """
+ Set the value choices for the filter.
+
+ If ``choices`` is non-empty, it is passed to
+ :meth:`normalize_choices()` and the result is assigned to
+ :attr:`choices`. Also, the :attr:`data_type` is set to
+ ``'choice'`` so the UI will present the value input as a
+ dropdown.
+
+ But if ``choices`` is empty, :attr:`choices` is set to an
+ empty dict, and :attr:`data_type` is set (back) to
+ ``'string'``.
+
+ :param choices: Collection of "choices" or ``None``.
+ """
+ if choices:
+ self.choices = self.normalize_choices(choices)
+ self.data_type = 'choice'
+ else:
+ self.choices = {}
+ self.data_type = 'string'
+
+ def normalize_choices(self, choices):
+ """
+ Normalize a collection of "choices" to standard ``OrderedDict``.
+
+ This is called automatically by :meth:`set_choices()`.
+
+ :param choices: A collection of "choices" in one of the following
+ formats:
+
+ * :class:`python:enum.Enum` class
+ * simple list, each value of which should be a string,
+ which is assumed to be able to serve as both key and
+ value (ordering of choices will be preserved)
+ * simple dict, keys and values of which will define the
+ choices (note that the final choices will be sorted by
+ key!)
+ * OrderedDict, keys and values of which will define the
+ choices (ordering of choices will be preserved)
+
+ :rtype: :class:`python:collections.OrderedDict`
+ """
+ normalized = choices
+
+ if isinstance(choices, EnumType):
+ normalized = OrderedDict([
+ (member.name, member.value)
+ for member in choices])
+
+ elif isinstance(choices, OrderedDict):
+ normalized = choices
+
+ elif isinstance(choices, dict):
+ normalized = OrderedDict([
+ (key, choices[key])
+ for key in sorted(choices)])
+
+ elif isinstance(choices, list):
+ normalized = OrderedDict([
+ (key, key)
+ for key in choices])
+
+ return normalized
+
def apply_filter(self, data, verb=None, value=UNSPECIFIED):
"""
Filter the given data set according to a verb/value pair.
diff --git a/src/wuttaweb/templates/wutta-components.mako b/src/wuttaweb/templates/wutta-components.mako
index 81cfcb3..59f7d91 100644
--- a/src/wuttaweb/templates/wutta-components.mako
+++ b/src/wuttaweb/templates/wutta-components.mako
@@ -486,6 +486,17 @@
v-show="valuedVerb()"
:is-small="isSmall" />
+
+
+
+