From a612bf3846b0df6a3f2ef5af255497c32675cec9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 2 Jan 2025 20:13:04 -0600 Subject: [PATCH] fix: add grid renderers for bool, currency, quantity also set bool renderer by default when possible --- src/wuttaweb/grids/base.py | 76 +++++++++++++++++++++++++++++++------- tests/grids/test_base.py | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index b9f0de7..2259c3f 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -116,7 +116,8 @@ class Grid: Dict of column (cell) value renderer overrides. - See also :meth:`set_renderer()`. + See also :meth:`set_renderer()` and + :meth:`set_default_renderers()`. .. attribute:: row_class @@ -602,15 +603,18 @@ class Grid: """ Set default column value renderers, where applicable. - This will add new entries to :attr:`renderers` for columns - whose data type implies a default renderer should be used. - This is generally only possible if :attr:`model_class` is set - to a valid SQLAlchemy mapped class. + This is called automatically from the class constructor. It + will add new entries to :attr:`renderers` for columns whose + data type implies a default renderer. This is only possible + if :attr:`model_class` is set to a SQLAlchemy mapped class. - This (for now?) only looks for - :class:`sqlalchemy:sqlalchemy.types.DateTime` columns and if - any are found, they are configured to use - :meth:`render_datetime()`. + This only looks for a couple of data types, and configures as + follows: + + * :class:`sqlalchemy:sqlalchemy.types.Boolean` -> + :meth:`render_boolean()` + * :class:`sqlalchemy:sqlalchemy.types.DateTime` -> + :meth:`render_datetime()` """ if not self.model_class: return @@ -626,6 +630,8 @@ class Grid: column = prop.columns[0] if isinstance(column.type, sa.DateTime): self.set_renderer(key, self.render_datetime) + elif isinstance(column.type, sa.Boolean): + self.set_renderer(key, self.render_boolean) def set_link(self, key, link=True): """ @@ -1753,23 +1759,65 @@ class Grid: # rendering methods ############################## + def render_boolean(self, obj, key, value): + """ + Column renderer for boolean values. + + This calls + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_boolean()` + for the return value. + + This may be used automatically per + :meth:`set_default_renderers()` or you can use it explicitly:: + + grid.set_renderer('foo', grid.render_boolean) + """ + return self.app.render_boolean(value) + + def render_currency(self, obj, key, value): + """ + Column renderer for currency values. + + This calls + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_currency()` + for the return value. + + This is not used automatically but you can use it explicitly:: + + grid.set_renderer('foo', grid.render_currency) + """ + return self.app.render_currency(value) + def render_datetime(self, obj, key, value): """ - Default cell value renderer for - :class:`sqlalchemy:sqlalchemy.types.DateTime` columns, which - calls + Column renderer for :class:`python:datetime.datetime` values. + + This calls :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_datetime()` for the return value. This may be used automatically per - :meth:`set_default_renderers()` or you can use it explicitly - for any :class:`python:datetime.datetime` column with:: + :meth:`set_default_renderers()` or you can use it explicitly:: grid.set_renderer('foo', grid.render_datetime) """ dt = getattr(obj, key) return self.app.render_datetime(dt) + def render_quantity(self, obj, key, value): + """ + Column renderer for quantity values. + + This calls + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()` + for the return value. + + This is not used automatically but you can use it explicitly:: + + grid.set_renderer('foo', grid.render_quantity) + """ + return self.app.render_quantity(value) + def render_table_element( self, form=None, diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py index f03aad2..28478d2 100644 --- a/tests/grids/test_base.py +++ b/tests/grids/test_base.py @@ -1,6 +1,7 @@ # -*- coding: utf-8; -*- import datetime +import decimal from unittest import TestCase from unittest.mock import patch, MagicMock @@ -232,6 +233,17 @@ class TestGrid(WebTestCase): self.assertIn('created', grid.renderers) self.assertIs(grid.renderers['created'], myrender) + # renderer set for boolean mapped field + grid = self.make_grid(model_class=model.Upgrade) + self.assertIn('executing', grid.renderers) + self.assertIsNot(grid.renderers['executing'], myrender) + + # renderer *not* set for boolean, if override present + grid = self.make_grid(model_class=model.Upgrade, + renderers={'executing': myrender}) + self.assertIn('executing', grid.renderers) + self.assertIs(grid.renderers['executing'], myrender) + def test_linked_columns(self): grid = self.make_grid(columns=['foo', 'bar']) self.assertEqual(grid.linked_columns, []) @@ -1331,6 +1343,62 @@ class TestGrid(WebTestCase): # rendering methods ############################## + def test_render_boolean(self): + grid = self.make_grid(columns=['foo', 'bar']) + + # null + obj = MagicMock(foo=None) + self.assertEqual(grid.render_boolean(obj, 'foo', None), "") + + # true + obj = MagicMock(foo=True) + self.assertEqual(grid.render_boolean(obj, 'foo', True), "Yes") + + # false + obj = MagicMock(foo=False) + self.assertEqual(grid.render_boolean(obj, 'foo', False), "No") + + def test_render_currency(self): + grid = self.make_grid(columns=['foo', 'bar']) + obj = MagicMock() + + # null + self.assertEqual(grid.render_currency(obj, 'foo', None), '') + + # basic decimal example + value = decimal.Decimal('42.00') + self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00') + + # basic float example + value = 42.00 + self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00') + + # decimal places will be rounded + value = decimal.Decimal('42.12345') + self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.12') + + # negative numbers get parens + value = decimal.Decimal('-42.42') + self.assertEqual(grid.render_currency(obj, 'foo', value), '($42.42)') + + def test_render_quantity(self): + grid = self.make_grid(columns=['foo', 'bar']) + obj = MagicMock() + + # null + self.assertEqual(grid.render_quantity(obj, 'foo', None), "") + + # integer decimals become integers + value = decimal.Decimal('1.000') + self.assertEqual(grid.render_quantity(obj, 'foo', value), "1") + + # but decimal places are preserved + value = decimal.Decimal('1.234') + self.assertEqual(grid.render_quantity(obj ,'foo', value), "1.234") + + # zero is *not* empty string (with this renderer) + self.assertEqual(grid.render_quantity(obj, 'foo', 0), "0") + def test_render_datetime(self): grid = self.make_grid(columns=['foo', 'bar'])