From d63a9223d360d9a8bd3ae4edd34dc43c686b7205 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 12 Jan 2025 17:11:46 -0600 Subject: [PATCH 1/4] fix: use default value for config settings --- src/wuttaweb/views/master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index 6876603..cdea8c7 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -1224,7 +1224,7 @@ class MasterView(View): elif simple.get('type') is bool: value = self.config.get_bool(name, default=simple.get('default', False)) else: - value = self.config.get(name) + value = self.config.get(name, default=simple.get('default')) normalized[name] = value From c33f211633a1bce19d62aa3e41059620407a35d1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 12 Jan 2025 19:12:53 -0600 Subject: [PATCH 2/4] fix: add grid filters specific to numeric, integer types --- src/wuttaweb/grids/filters.py | 29 +++++++++++++++++++++++++++++ tests/grids/test_filters.py | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/wuttaweb/grids/filters.py b/src/wuttaweb/grids/filters.py index 6be29c7..1250b3d 100644 --- a/src/wuttaweb/grids/filters.py +++ b/src/wuttaweb/grids/filters.py @@ -465,6 +465,33 @@ class StringAlchemyFilter(AlchemyFilter): sa.and_(*criteria))) +class NumericAlchemyFilter(AlchemyFilter): + """ + SQLAlchemy filter option for a numeric data column. + + Subclass of :class:`AlchemyFilter`. + """ + default_verbs = ['equal', 'not_equal', + 'greater_than', 'greater_equal', + 'less_than', 'less_equal'] + + +class IntegerAlchemyFilter(NumericAlchemyFilter): + """ + SQLAlchemy filter option for an integer data column. + + Subclass of :class:`NumericAlchemyFilter`. + """ + + def coerce_value(self, value): + """ """ + if value: + try: + return int(value) + except: + pass + + class BooleanAlchemyFilter(AlchemyFilter): """ SQLAlchemy filter option for a boolean data column. @@ -568,6 +595,8 @@ default_sqlalchemy_filters = { None: AlchemyFilter, sa.String: StringAlchemyFilter, sa.Text: StringAlchemyFilter, + sa.Numeric: NumericAlchemyFilter, + sa.Integer: IntegerAlchemyFilter, sa.Boolean: BooleanAlchemyFilter, sa.Date: DateAlchemyFilter, } diff --git a/tests/grids/test_filters.py b/tests/grids/test_filters.py index bbc6611..557dbff 100644 --- a/tests/grids/test_filters.py +++ b/tests/grids/test_filters.py @@ -326,6 +326,30 @@ class TestStringAlchemyFilter(WebTestCase): self.assertEqual(filtered_query.count(), 6) +class TestIntegerAlchemyFilter(WebTestCase): + + def make_filter(self, model_property, **kwargs): + factory = kwargs.pop('factory', mod.IntegerAlchemyFilter) + kwargs['model_property'] = model_property + return factory(self.request, model_property.key, **kwargs) + + def test_coerce_value(self): + model = self.app.model + filtr = self.make_filter(model.Upgrade.exit_code) + + # null + self.assertIsNone(filtr.coerce_value(None)) + self.assertIsNone(filtr.coerce_value('')) + + # typical + self.assertEqual(filtr.coerce_value('42'), 42) + self.assertEqual(filtr.coerce_value('-42'), -42) + + # invalid + self.assertIsNone(filtr.coerce_value('42.12')) + self.assertIsNone(filtr.coerce_value('bogus')) + + class TestBooleanAlchemyFilter(WebTestCase): def setUp(self): From 8ba44e10bd4a0e1fc5d94848e35bfa1ba9d4404a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 12 Jan 2025 19:42:22 -0600 Subject: [PATCH 3/4] fix: use prop key instead of column name, for master view model key every once in a while those can differ, we need prop key when they do --- src/wuttaweb/views/master.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index cdea8c7..cb32ef9 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -2707,6 +2707,9 @@ class MasterView(View): represents a Wutta-based SQLAlchemy model, the return value for this method is: ``('uuid',)`` + Any class mapped via SQLAlchemy should be supported + automatically, the keys are determined from class inspection. + But there is no "sane" default for other scenarios, in which case subclass should define :attr:`model_key`. If the model key cannot be determined, raises ``AttributeError``. @@ -2721,8 +2724,12 @@ class MasterView(View): model_class = cls.get_model_class() if model_class: - mapper = sa.inspect(model_class) - return tuple([column.key for column in mapper.primary_key]) + # nb. we want the primary key but must avoid column names + # in case mapped class uses different prop keys + inspector = sa.inspect(model_class) + keys = [col.name for col in inspector.primary_key] + return tuple([prop.key for prop in inspector.column_attrs + if [col.name for col in prop.columns] == keys]) raise AttributeError(f"you must define model_key for view class: {cls}") From 2b3d69a3794f941e4c360304e4c5f84f2d97d841 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jan 2025 12:55:34 -0600 Subject: [PATCH 4/4] fix: expose setting to choose menu handler, in appinfo/configure --- pyproject.toml | 3 + src/wuttaweb/handler.py | 82 +++++++++++++++++-- src/wuttaweb/templates/appinfo/configure.mako | 17 ++++ src/wuttaweb/views/settings.py | 16 +++- tests/test_handler.py | 75 ++++++++++++++++- 5 files changed, 181 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5a11bc1..a7a19ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,9 @@ wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension" [project.entry-points."wutta.typer_imports"] wuttaweb = "wuttaweb.cli" +[project.entry-points."wutta.web.menus"] +wuttaweb = "wuttaweb.menus:MenuHandler" + [project.urls] Homepage = "https://wuttaproject.org/" diff --git a/src/wuttaweb/handler.py b/src/wuttaweb/handler.py index 1ac0b78..9ca6c5a 100644 --- a/src/wuttaweb/handler.py +++ b/src/wuttaweb/handler.py @@ -24,7 +24,10 @@ Web Handler """ +import warnings + from wuttjamaican.app import GenericHandler +from wuttjamaican.util import load_entry_points from wuttaweb import static, forms, grids @@ -106,22 +109,87 @@ class WebHandler(GenericHandler): def get_menu_handler(self, **kwargs): """ - Get the configured "menu" handler for the web app. + Get the configured :term:`menu handler` for the web app. Specify a custom handler in your config file like this: .. code-block:: ini [wutta.web] - menus.handler_spec = poser.web.menus:PoserMenuHandler + menus.handler.spec = poser.web.menus:PoserMenuHandler :returns: Instance of :class:`~wuttaweb.menus.MenuHandler`. """ - if not hasattr(self, 'menu_handler'): - spec = self.config.get(f'{self.appname}.web.menus.handler_spec', - default='wuttaweb.menus:MenuHandler') - self.menu_handler = self.app.load_object(spec)(self.config) - return self.menu_handler + spec = self.config.get(f'{self.appname}.web.menus.handler.spec') + if not spec: + spec = self.config.get(f'{self.appname}.web.menus.handler_spec') + if spec: + warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; " + f"please use '{self.appname}.web.menus.handler_spec' instead", + DeprecationWarning) + else: + spec = self.config.get(f'{self.appname}.web.menus.handler.default_spec', + default='wuttaweb.menus:MenuHandler') + factory = self.app.load_object(spec) + return factory(self.config) + + def get_menu_handler_specs(self, default=None): + """ + Get the :term:`spec` strings for all available :term:`menu + handlers `. See also + :meth:`get_menu_handler()`. + + :param default: Default spec string(s) to include, even if not + registered. Can be a string or list of strings. + + :returns: List of menu handler spec strings. + + This will gather available spec strings from the following: + + First, the ``default`` as provided by caller. + + Second, the default spec from config, if set; for example: + + .. code-block:: ini + + [wutta.web] + menus.handler.default_spec = poser.web.menus:PoserMenuHandler + + Third, each spec registered via entry points. For instance in + ``pyproject.toml``: + + .. code-block:: toml + + [project.entry-points."wutta.web.menus"] + poser = "poser.web.menus:PoserMenuHandler" + + The final list will be "sorted" according to the above, with + the latter registered handlers being sorted alphabetically. + """ + handlers = [] + + # defaults from caller + if isinstance(default, str): + handlers.append(default) + elif default: + handlers.extend(default) + + # configured default, if applicable + default = self.config.get(f'{self.config.appname}.web.menus.handler.default_spec') + if default and default not in handlers: + handlers.append(default) + + # registered via entry points + registered = [] + for Handler in load_entry_points(f'{self.appname}.web.menus').values(): + spec = Handler.get_spec() + if spec not in handlers: + registered.append(spec) + if registered: + registered.sort() + handlers.extend(registered) + + return handlers def make_form(self, request, **kwargs): """ diff --git a/src/wuttaweb/templates/appinfo/configure.mako b/src/wuttaweb/templates/appinfo/configure.mako index 03f1551..f761ade 100644 --- a/src/wuttaweb/templates/appinfo/configure.mako +++ b/src/wuttaweb/templates/appinfo/configure.mako @@ -41,6 +41,21 @@ + + + + + + + +

User/Auth

@@ -241,6 +256,8 @@ ${parent.modify_vue_vars()}