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" /> + + + +