From e332975ce9deb1eac659a9e936a8e49c21113168 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 23 Aug 2024 14:14:41 -0500 Subject: [PATCH] feat: add per-row css class support for grids --- src/wuttaweb/forms/base.py | 8 +- src/wuttaweb/grids/base.py | 149 ++++++++++++------ .../templates/forms/vue_template.mako | 6 +- .../templates/grids/table_element.mako | 2 +- .../templates/grids/vue_template.mako | 11 +- src/wuttaweb/views/master.py | 14 +- src/wuttaweb/views/users.py | 19 ++- tests/forms/test_base.py | 28 ++-- tests/grids/test_base.py | 66 +++++++- tests/views/test_master.py | 14 ++ tests/views/test_users.py | 11 ++ 11 files changed, 253 insertions(+), 75 deletions(-) diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index d5b893a..c3567b5 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -313,7 +313,7 @@ class Form: self.set_fields(fields or self.get_fields()) # nb. this tracks grid JSON data for inclusion in page template - self.grid_vue_data = OrderedDict() + self.grid_vue_context = OrderedDict() def __contains__(self, name): """ @@ -826,16 +826,16 @@ class Form: output = render(template, context) return HTML.literal(output) - def add_grid_vue_data(self, grid): + def add_grid_vue_context(self, grid): """ """ if not grid.key: raise ValueError("grid must have a key!") - if grid.key in self.grid_vue_data: + if grid.key in self.grid_vue_context: log.warning("grid data with key '%s' already registered, " "but will be replaced", grid.key) - self.grid_vue_data[grid.key] = grid.get_vue_data() + self.grid_vue_context[grid.key] = grid.get_vue_context() def render_vue_field( self, diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 0ec15f3..aa2e413 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -27,6 +27,7 @@ Base grid classes import functools import json import logging +import warnings from collections import namedtuple import sqlalchemy as sa @@ -116,6 +117,26 @@ class Grid: See also :meth:`set_renderer()`. + .. attribute:: row_class + + This represents the CSS ``class`` attribute for a row within + the grid. Default is ``None``. + + This can be a simple string, in which case the same class is + applied to all rows. + + Or it can be a callable, which can then return different + class(es) depending on each row. The callable must take three + args: ``(obj, data, i)`` - for example:: + + def my_row_class(obj, data, i): + if obj.archived: + return 'poser-archived' + + grid = Grid(request, key='foo', row_class=my_row_class) + + See :meth:`get_row_class()` for more info. + .. attribute:: actions List of :class:`GridAction` instances represenging action links @@ -330,6 +351,7 @@ class Grid: data=None, labels={}, renderers={}, + row_class=None, actions=[], linked_columns=[], sortable=False, @@ -355,6 +377,7 @@ class Grid: self.data = data self.labels = labels or {} self.renderers = renderers or {} + self.row_class = row_class self.actions = actions or [] self.linked_columns = linked_columns or [] self.joiners = joiners or {} @@ -530,8 +553,9 @@ class Grid: Depending on the nature of grid data, sometimes a cell's "as-is" value will be undesirable for display purposes. - The logic in :meth:`get_vue_data()` will first "convert" all - grid data as necessary so that it is at least JSON-compatible. + The logic in :meth:`get_vue_context()` will first "convert" + all grid data as necessary so that it is at least + JSON-compatible. But then it also will invoke a renderer override (if defined) to obtain the "final" cell value. @@ -1670,7 +1694,7 @@ class Grid: .. code-block:: html - + @@ -1689,10 +1713,10 @@ class Grid: .. note:: - The above example shows ``gridData['mykey']`` as the Vue - data reference. This should "just work" if you provide the - correct ``form`` arg and the grid is contained directly by - that form's Vue component. + The above example shows ``gridContext['mykey'].data`` as + the Vue data reference. This should "just work" if you + provide the correct ``form`` arg and the grid is contained + directly by that form's Vue component. However, this may not account for all use cases. For now we wait and see what comes up, but know the dust may not @@ -1701,7 +1725,7 @@ class Grid: # nb. must register data for inclusion on page template if form: - form.add_grid_vue_data(self) + form.add_grid_vue_context(self) # otherwise logic is the same, just different template return self.render_vue_template(template=template, **context) @@ -1809,7 +1833,7 @@ class Grid: in its `Table docs `_. - See also :meth:`get_vue_data()`. + See also :meth:`get_vue_context()`. """ if not self.columns: raise ValueError(f"you must define columns for the grid! key = {self.key}") @@ -1869,54 +1893,46 @@ class Grid: }) return filters - def get_vue_data(self): + def get_vue_context(self): """ - Returns a list of Vue-compatible data records. + Returns a dict of context for the grid, for use with the Vue + component. This contains the following keys: - This calls :meth:`get_visible_data()` but then may modify the - result, e.g. to add URLs for :attr:`actions` etc. + * ``data`` - list of Vue-compatible data records + * ``row_classes`` - dict of per-row CSS classes - Importantly, this also ensures each value in the dict is - JSON-serializable, using - :func:`~wuttaweb.util.make_json_safe()`. + This first calls :meth:`get_visible_data()` to get the + original data set. Each record is converted to a dict. - :returns: List of data record dicts for use with Vue table - component. May be the full set of data, or just the - current page, per :attr:`paginate_on_backend`. + Then it calls :func:`~wuttaweb.util.make_json_safe()` to + ensure each record can be serialized to JSON. + + Then it invokes any :attr:`renderers` which are defined, to + obtain the "final" values for each record. + + Then it adds a URL key/value for each of the :attr:`actions` + defined, to each record. + + Then it calls :meth:`get_row_class()` for each record. If a + value is returned, it is added to the ``row_classes`` dict. + Note that this dict is keyed by "zero-based row sequence as + string" - the Vue component expects that. + + :returns: Dict of grid data/CSS context as described above. """ original_data = self.get_visible_data() - # TODO: at some point i thought it was useful to wrangle the - # columns here, but now i can't seem to figure out why..? - - # # determine which columns are relevant for data set - # columns = None - # if not columns: - # columns = self.get_columns() - # if not columns: - # raise ValueError("cannot determine columns for the grid") - # columns = set(columns) - # if self.model_class: - # mapper = sa.inspect(self.model_class) - # for column in mapper.primary_key: - # columns.add(column.key) - - # # prune data fields for which no column is defined - # for i, record in enumerate(original_data): - # original_data[i]= dict([(key, record[key]) - # for key in columns]) - - # we have action(s), so add URL(s) for each record in data + # loop thru data data = [] - for i, record in enumerate(original_data): + row_classes = {} + for i, record in enumerate(original_data, 1): original_record = record + # convert record to new dict record = dict(record) - # convert data if needed, for json compat - record = make_json_safe(record, - # TODO: is this a good idea? - warn=False) + # make all values safe for json + record = make_json_safe(record, warn=False) # customize value rendering where applicable for key in self.renderers: @@ -1931,9 +1947,48 @@ class Grid: if url: record[key] = url + # set row css class if applicable + css_class = self.get_row_class(original_record, record, i) + if css_class: + # nb. use *string* zero-based index, for js compat + row_classes[str(i-1)] = css_class + data.append(record) - return data + return { + 'data': data, + 'row_classes': row_classes, + } + + def get_vue_data(self): + """ """ + warnings.warn("grid.get_vue_data() is deprecated; " + "please use grid.get_vue_context() instead", + DeprecationWarning, stacklevel=2) + return self.get_vue_context()['data'] + + def get_row_class(self, obj, data, i): + """ + Returns the row CSS ``class`` attribute for the given record. + This method is called by :meth:`get_vue_context()`. + + This will inspect/invoke :attr:`row_class` and return the + value obtained from there. + + :param obj: Reference to the original model instance. + + :param data: Dict of record data for the instance; part of the + Vue grid data set in/from :meth:`get_vue_context()`. + + :param i: One-based sequence for this object/record (row) + within the grid. + + :returns: String of CSS class name(s), or ``None``. + """ + if self.row_class: + if callable(self.row_class): + return self.row_class(obj, data, i) + return self.row_class def get_vue_pager_stats(self): """ @@ -2086,7 +2141,7 @@ class GridAction: :param obj: Model instance of whatever type the parent grid is setup to use. - :param i: Zero-based sequence for the object, within the + :param i: One-based sequence for the object's row within the parent grid. See also :attr:`url`. diff --git a/src/wuttaweb/templates/forms/vue_template.mako b/src/wuttaweb/templates/forms/vue_template.mako index e7d3f2b..5a4af70 100644 --- a/src/wuttaweb/templates/forms/vue_template.mako +++ b/src/wuttaweb/templates/forms/vue_template.mako @@ -69,9 +69,9 @@ % endif - % if form.grid_vue_data: - gridData: { - % for key, data in form.grid_vue_data.items(): + % if form.grid_vue_context: + gridContext: { + % for key, data in form.grid_vue_context.items(): '${key}': ${json.dumps(data)|n}, % endfor }, diff --git a/src/wuttaweb/templates/grids/table_element.mako b/src/wuttaweb/templates/grids/table_element.mako index ba35bf3..1bbf8a9 100644 --- a/src/wuttaweb/templates/grids/table_element.mako +++ b/src/wuttaweb/templates/grids/table_element.mako @@ -1,5 +1,5 @@ ## -*- coding: utf-8; -*- -<${b}-table :data="gridData['${grid.key}']"> +<${b}-table :data="gridContext['${grid.key}'].data"> % for column in grid.get_vue_columns(): <${b}-table-column field="${column['field']}" diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako index e701a4a..edcdc24 100644 --- a/src/wuttaweb/templates/grids/vue_template.mako +++ b/src/wuttaweb/templates/grids/vue_template.mako @@ -93,6 +93,7 @@ <${b}-table :data="data" + :row-class="getRowClass" :loading="loading" narrowed hoverable @@ -227,10 +228,12 @@