diff --git a/CHANGELOG.md b/CHANGELOG.md index 646afbd..7f0025a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to wuttaweb will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.19.2 (2025-01-07) + +### Fix + +- always use prop key for default grid filters +- avoid `request.current_route_url()` for user menu +- add `scale` kwarg for `WuttaMoney` schema type, widget +- make WuttaQuantity serialize w/ app handler, remove custom widget +- bugfix for bool simple settings with default value + ## v0.19.1 (2025-01-06) ### Fix diff --git a/pyproject.toml b/pyproject.toml index f1bb921..d80e1d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.19.1" +version = "0.19.2" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -42,7 +42,6 @@ dependencies = [ "pyramid_fanstatic", "pyramid_mako", "pyramid_tm", - "SQLAlchemy-Utils", "waitress", "WebHelpers2", "WuttJamaican[db]>=0.19.2", diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 3d0e08b..e4c3703 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -164,9 +164,13 @@ class WuttaMoney(colander.Money): by default. :param request: Current :term:`request` object. + + :param scale: If this kwarg is specified, it will be passed along + to the widget constructor. """ def __init__(self, request, *args, **kwargs): + self.scale = kwargs.pop('scale', None) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -174,6 +178,8 @@ class WuttaMoney(colander.Money): def widget_maker(self, **kwargs): """ """ + if self.scale: + kwargs.setdefault('scale', self.scale) return widgets.WuttaMoneyInputWidget(self.request, **kwargs) @@ -181,8 +187,9 @@ class WuttaQuantity(colander.Decimal): """ Custom schema type for "quantity" fields. - This is a subclass of :class:`colander:colander.Decimal` but uses - :class:`~wuttaweb.forms.widgets.WuttaQuantityWidget` by default. + This is a subclass of :class:`colander:colander.Decimal` but will + serialize values via + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()`. :param request: Current :term:`request` object. """ @@ -193,9 +200,14 @@ class WuttaQuantity(colander.Decimal): self.config = self.request.wutta_config self.app = self.config.get_app() - def widget_maker(self, **kwargs): + def serialize(self, node, appstruct): """ """ - return widgets.WuttaQuantityWidget(self.request, **kwargs) + if appstruct in (colander.null, None): + return colander.null + + # nb. we render as quantity here to avoid values like 12.0000, + # so we just show value like 12 instead + return self.app.render_quantity(appstruct) class WuttaSet(colander.Set): diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index a6f33d2..1d3035c 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -206,9 +206,13 @@ class WuttaMoneyInputWidget(MoneyInputWidget): * ``moneyinput`` :param request: Current :term:`request` object. + + :param scale: If this kwarg is specified, it will be passed along + to ``render_currency()`` call. """ def __init__(self, request, *args, **kwargs): + self.scale = kwargs.pop('scale', 2) super().__init__(*args, **kwargs) self.request = request self.config = self.request.wutta_config @@ -221,43 +225,8 @@ class WuttaMoneyInputWidget(MoneyInputWidget): if cstruct in (colander.null, None): return HTML.tag('span') cstruct = decimal.Decimal(cstruct) - return HTML.tag('span', c=[self.app.render_currency(cstruct)]) - - return super().serialize(field, cstruct, **kw) - - -class WuttaQuantityWidget(TextInputWidget): - """ - Custom widget for "quantity" fields. This is used by default for - :class:`~wuttaweb.forms.schema.WuttaQuantity` type nodes. - - The main purpose of this widget is to leverage - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()` - for the readonly display. - - This is a subclass of - :class:`deform:deform.widget.TextInputWidget` and uses these - Deform templates: - - * ``textinput`` - - :param request: Current :term:`request` object. - """ - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.config = self.request.wutta_config - self.app = self.config.get_app() - - def serialize(self, field, cstruct, **kw): - """ """ - readonly = kw.get('readonly', self.readonly) - if readonly: - if cstruct in (colander.null, None): - return HTML.tag('span') - cstruct = decimal.Decimal(cstruct) - return HTML.tag('span', c=[self.app.render_quantity(cstruct)]) + text = self.app.render_currency(cstruct, scale=self.scale) + return HTML.tag('span', c=[text]) return super().serialize(field, cstruct, **kw) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 3a3d4f5..c2f3156 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -32,7 +32,6 @@ from collections import namedtuple, OrderedDict import sqlalchemy as sa from sqlalchemy import orm -from sqlalchemy_utils import get_columns import paginate from paginate_sqlalchemy import SqlalchemyOrmPage @@ -1147,16 +1146,29 @@ class Grid: filters = filters or {} if self.model_class: - # nb. i first tried self.get_model_columns() but my notes - # say that was too aggressive in many cases. then i tried - # using the *subset* of self.columns, just the ones which - # corresponded to a property on the model class. and now - # i am using sa-utils to give the "true" column list.. - for col in get_columns(self.model_class): - if col.key in filters: - continue - prop = getattr(self.model_class, col.key) - filters[prop.key] = self.make_filter(prop) + + # nb. i have found this confusing for some reason. some + # things i've tried so far include: + # + # i first tried self.get_model_columns() but my notes say + # that was too aggressive in many cases. + # + # then i tried using the *subset* of self.columns, just + # the ones which correspond to a property on the model + # class. but sometimes that skips filters we need. + # + # then i tried get_columns() from sa-utils to give the + # "true" column list, but that fails when the underlying + # column has different name than the prop/attr key. + # + # so now, we are looking directly at the sa mapper, for + # all column attrs and then using the prop key. + + inspector = sa.inspect(self.model_class) + for prop in inspector.column_attrs: + if prop.key not in filters: + attr = getattr(self.model_class, prop.key) + filters[prop.key] = self.make_filter(attr) return filters diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako index 425d5bd..429e9be 100644 --- a/src/wuttaweb/templates/base.mako +++ b/src/wuttaweb/templates/base.mako @@ -659,7 +659,7 @@ % if request.is_root: ${h.form(url('stop_root'), ref='stopBeingRootForm')} ${h.csrf_token(request)} - + Stop being root diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index 3701980..cedb1cf 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -1329,6 +1329,9 @@ class MasterView(View): if name in data: value = data[name] + elif simple.get('type') is bool: + # nb. bool false will be *missing* from data + value = False else: value = simple.get('default') diff --git a/tests/forms/__init__.py b/tests/forms/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py index 80a40e2..2a37aa5 100644 --- a/tests/forms/test_schema.py +++ b/tests/forms/test_schema.py @@ -1,6 +1,7 @@ # -*- coding: utf-8; -*- import datetime +import decimal from unittest import TestCase from unittest.mock import patch @@ -15,7 +16,7 @@ from wuttaweb.forms import widgets from wuttaweb.testing import DataTestCase, WebTestCase -class TestWutaDateTime(TestCase): +class TestWuttaDateTime(TestCase): def test_deserialize(self): typ = mod.WuttaDateTime() @@ -84,18 +85,41 @@ class TestWuttaMoney(WebTestCase): def test_widget_maker(self): enum = self.app.enum + + # default scale typ = mod.WuttaMoney(self.request) widget = typ.widget_maker() self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget) + self.assertEqual(widget.scale, 2) + + # custom scale + typ = mod.WuttaMoney(self.request, scale=4) + widget = typ.widget_maker() + self.assertIsInstance(widget, widgets.WuttaMoneyInputWidget) + self.assertEqual(widget.scale, 4) class TestWuttaQuantity(WebTestCase): - def test_widget_maker(self): - enum = self.app.enum - typ = mod.WuttaQuantity(self.request) - widget = typ.widget_maker() - self.assertIsInstance(widget, widgets.WuttaQuantityWidget) + def test_serialize(self): + node = colander.SchemaNode(mod.WuttaQuantity(self.request)) + typ = node.typ + + # null + result = typ.serialize(node, colander.null) + self.assertIs(result, colander.null) + result = typ.serialize(node, None) + self.assertIs(result, colander.null) + + # quantity + result = typ.serialize(node, 42) + self.assertEqual(result, '42') + result = typ.serialize(node, 42.00) + self.assertEqual(result, '42') + result = typ.serialize(node, decimal.Decimal('42.00')) + self.assertEqual(result, '42') + result = typ.serialize(node, 42.13) + self.assertEqual(result, '42.13') class TestObjectRef(DataTestCase): diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index 47aed58..4874c25 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -143,36 +143,6 @@ class TestWuttaMoneyInputWidget(WebTestCase): self.assertEqual(result, '') -class TestWuttaQuantityWidget(WebTestCase): - - def make_field(self, node, **kwargs): - # TODO: not sure why default renderer is in use even though - # pyramid_deform was included in setup? but this works.. - kwargs.setdefault('renderer', deform.Form.default_renderer) - return deform.Field(node, **kwargs) - - def make_widget(self, **kwargs): - return mod.WuttaQuantityWidget(self.request, **kwargs) - - def test_serialize(self): - node = colander.SchemaNode(schema.WuttaQuantity(self.request)) - field = self.make_field(node) - widget = self.make_widget() - amount = decimal.Decimal('42.00') - - # editable widget has normal text input - result = widget.serialize(field, str(amount)) - self.assertIn('42') - - # readonly w/ null value - result = widget.serialize(field, None, readonly=True) - self.assertEqual(result, '') - - class TestFileDownloadWidget(WebTestCase): def make_field(self, node, **kwargs): diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 56c51c2..d334258 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -1565,6 +1565,38 @@ class TestMasterView(WebTestCase): count = self.session.query(model.Setting).count() self.assertEqual(count, 0) + def test_configure_gather_settings(self): + view = self.make_view() + + simple_settings = [ + {'name': 'wutta.app_title'}, + {'name': 'wutta.foo'}, + {'name': 'wutta.flag', 'type': bool, 'default': True}, + {'name': 'wutta.number', 'type': int, 'default': 42}, + {'name': 'wutta.value1', 'save_if_empty': True}, + {'name': 'wutta.value2', 'save_if_empty': False}, + {'name': 'wutta.value3', 'save_if_empty': False, 'default': 'baz'}, + ] + + data = { + 'wutta.app_title': 'Poser', + 'wutta.foo': 'bar', + 'wutta.number': 44, + 'wutta.value1': None, + } + + with patch.object(view, 'configure_get_simple_settings', return_value=simple_settings): + settings = view.configure_gather_settings(data) + self.assertEqual(len(settings), 6) + self.assertEqual(settings, [ + {'name': 'wutta.app_title', 'value': 'Poser'}, + {'name': 'wutta.foo', 'value': 'bar'}, + {'name': 'wutta.flag', 'value': 'false'}, + {'name': 'wutta.number', 'value': '44'}, + {'name': 'wutta.value1', 'value': ''}, + {'name': 'wutta.value3', 'value': 'baz'}, + ]) + ############################## # row methods ##############################