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:
parent
8357fd594f
commit
37ae69de00
5 changed files with 291 additions and 1 deletions
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue