diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0025a..646afbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,6 @@ 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 d80e1d4..f1bb921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.19.2" +version = "0.19.1" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -42,6 +42,7 @@ 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 e4c3703..3d0e08b 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -164,13 +164,9 @@ 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 @@ -178,8 +174,6 @@ class WuttaMoney(colander.Money): def widget_maker(self, **kwargs): """ """ - if self.scale: - kwargs.setdefault('scale', self.scale) return widgets.WuttaMoneyInputWidget(self.request, **kwargs) @@ -187,9 +181,8 @@ class WuttaQuantity(colander.Decimal): """ Custom schema type for "quantity" fields. - This is a subclass of :class:`colander:colander.Decimal` but will - serialize values via - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_quantity()`. + This is a subclass of :class:`colander:colander.Decimal` but uses + :class:`~wuttaweb.forms.widgets.WuttaQuantityWidget` by default. :param request: Current :term:`request` object. """ @@ -200,14 +193,9 @@ class WuttaQuantity(colander.Decimal): self.config = self.request.wutta_config self.app = self.config.get_app() - def serialize(self, node, appstruct): + def widget_maker(self, **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) + return widgets.WuttaQuantityWidget(self.request, **kwargs) class WuttaSet(colander.Set): diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index 1d3035c..a6f33d2 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -206,13 +206,9 @@ 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 @@ -225,8 +221,43 @@ class WuttaMoneyInputWidget(MoneyInputWidget): if cstruct in (colander.null, None): return HTML.tag('span') cstruct = decimal.Decimal(cstruct) - text = self.app.render_currency(cstruct, scale=self.scale) - return HTML.tag('span', c=[text]) + 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)]) return super().serialize(field, cstruct, **kw) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index c2f3156..3a3d4f5 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -32,6 +32,7 @@ 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 @@ -1146,29 +1147,16 @@ class Grid: filters = filters or {} if self.model_class: - - # 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) + # 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) return filters diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako index 429e9be..425d5bd 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 cedb1cf..3701980 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -1329,9 +1329,6 @@ 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 new file mode 100644 index 0000000..e69de29 diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py index 2a37aa5..80a40e2 100644 --- a/tests/forms/test_schema.py +++ b/tests/forms/test_schema.py @@ -1,7 +1,6 @@ # -*- coding: utf-8; -*- import datetime -import decimal from unittest import TestCase from unittest.mock import patch @@ -16,7 +15,7 @@ from wuttaweb.forms import widgets from wuttaweb.testing import DataTestCase, WebTestCase -class TestWuttaDateTime(TestCase): +class TestWutaDateTime(TestCase): def test_deserialize(self): typ = mod.WuttaDateTime() @@ -85,41 +84,18 @@ 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_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') + def test_widget_maker(self): + enum = self.app.enum + typ = mod.WuttaQuantity(self.request) + widget = typ.widget_maker() + self.assertIsInstance(widget, widgets.WuttaQuantityWidget) class TestObjectRef(DataTestCase): diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py index 4874c25..47aed58 100644 --- a/tests/forms/test_widgets.py +++ b/tests/forms/test_widgets.py @@ -143,6 +143,36 @@ 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 d334258..56c51c2 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -1565,38 +1565,6 @@ 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 ##############################