3
0
Fork 0

fix: add value choice/enum support for grid filters

also add `set_enum()` method for grids, which updates column renderer
as well as filter choices
This commit is contained in:
Lance Edgar 2025-02-21 13:27:52 -06:00
parent 8357fd594f
commit 37ae69de00
5 changed files with 291 additions and 1 deletions

View file

@ -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,
})

View file

@ -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.

View file

@ -486,6 +486,17 @@
v-show="valuedVerb()"
:is-small="isSmall" />
<b-select v-if="filter.data_type == 'choice'"
v-model="filter.value"
ref="filterValue"
v-show="valuedVerb()">
<option v-for="choice in filter.choices"
:key="choice"
:value="choice">
{{ filter.choice_labels[choice] || choice }}
</option>
</b-select>
<wutta-filter-value v-else
v-model="filter.value"
ref="filterValue"