3
0
Fork 0

Compare commits

...

4 commits

8 changed files with 244 additions and 15 deletions

View file

@ -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/"

View file

@ -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,
}

View file

@ -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 <menu handler>`. 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):
"""

View file

@ -41,6 +41,21 @@
</b-checkbox>
</b-field>
<b-field label="Menu Handler">
<input type="hidden"
name="${app.appname}.web.menus.handler.spec"
:value="simpleSettings['${app.appname}.web.menus.handler.spec']" />
<b-select v-model="simpleSettings['${app.appname}.web.menus.handler.spec']"
@input="settingsNeedSaved = true">
<option :value="null">(use default)</option>
<option v-for="handler in menuHandlers"
:key="handler.spec"
:value="handler.spec">
{{ handler.spec }}
</option>
</b-select>
</b-field>
</div>
<h3 class="block is-size-3">User/Auth</h3>
@ -241,6 +256,8 @@
${parent.modify_vue_vars()}
<script>
ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
ThisPageData.editWebLibraryShowDialog = false

View file

@ -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
@ -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}")

View file

@ -129,6 +129,10 @@ class AppInfoView(MasterView):
{'name': f'{self.config.appname}.node_title'},
{'name': f'{self.config.appname}.production',
'type': bool},
{'name': f'{self.config.appname}.web.menus.handler.spec'},
# nb. this is deprecated; we define so it is auto-deleted
# when we replace with newer setting
{'name': f'{self.config.appname}.web.menus.handler_spec'},
# user/auth
{'name': 'wuttaweb.home_redirect_to_login',
@ -164,11 +168,15 @@ class AppInfoView(MasterView):
def configure_get_context(self, **kwargs):
""" """
# normal context
context = super().configure_get_context(**kwargs)
# we will add `weblibs` to context, based on config values
# add registered menu handlers
web = self.app.get_web_handler()
handlers = web.get_menu_handler_specs()
handlers = [{'spec': spec} for spec in handlers]
context['menu_handlers'] = handlers
# add `weblibs` to context, based on config values
weblibs = self.get_weblibs()
for key in weblibs:
title = weblibs[key]
@ -192,8 +200,8 @@ class AppInfoView(MasterView):
'live_url': get_liburl(self.request, key,
prefix=self.weblib_config_prefix),
}
context['weblibs'] = list(weblibs.values())
return context

View file

@ -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):

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from wuttaweb import handler as mod, static
from wuttaweb.forms import Form
from wuttaweb.grids import Grid
@ -7,6 +9,16 @@ from wuttaweb.menus import MenuHandler
from wuttaweb.testing import WebTestCase
class MockMenuHandler(MenuHandler):
pass
class LegacyMenuHandler(MenuHandler):
pass
class AnotherMenuHandler(MenuHandler):
pass
class TestWebHandler(WebTestCase):
def make_handler(self):
@ -60,10 +72,71 @@ class TestWebHandler(WebTestCase):
url = handler.get_main_logo_url(self.request)
self.assertEqual(url, '/testing/other.png')
def test_menu_handler_default(self):
def test_get_menu_handler(self):
handler = self.make_handler()
# built-in default
menus = handler.get_menu_handler()
self.assertIsInstance(menus, MenuHandler)
self.assertIs(type(menus), MenuHandler)
# configured default
self.config.setdefault('wutta.web.menus.handler.default_spec',
'tests.test_handler:MockMenuHandler')
menus = handler.get_menu_handler()
self.assertIsInstance(menus, MockMenuHandler)
# configured handler (legacy)
self.config.setdefault('wutta.web.menus.handler_spec',
'tests.test_handler:LegacyMenuHandler')
menus = handler.get_menu_handler()
self.assertIsInstance(menus, LegacyMenuHandler)
# configued handler (proper)
self.config.setdefault('wutta.web.menus.handler.spec',
'tests.test_handler:AnotherMenuHandler')
menus = handler.get_menu_handler()
self.assertIsInstance(menus, AnotherMenuHandler)
def test_get_menu_handler_specs(self):
handler = self.make_handler()
# at least one spec by default
specs = handler.get_menu_handler_specs()
self.assertIn('wuttaweb.menus:MenuHandler', specs)
# caller can specify default as string
specs = handler.get_menu_handler_specs(default='tests.test_handler:MockMenuHandler')
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertIn('tests.test_handler:MockMenuHandler', specs)
self.assertNotIn('tests.test_handler:AnotherMenuHandler', specs)
# caller can specify default as list
specs = handler.get_menu_handler_specs(default=[
'tests.test_handler:MockMenuHandler',
'tests.test_handler:AnotherMenuHandler'])
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
# default can be configured
self.config.setdefault('wutta.web.menus.handler.default_spec',
'tests.test_handler:AnotherMenuHandler')
specs = handler.get_menu_handler_specs()
self.assertIn('wuttaweb.menus:MenuHandler', specs)
self.assertNotIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
# the rest come from entry points
with patch.object(mod, 'load_entry_points', return_value={
'legacy': LegacyMenuHandler,
}):
specs = handler.get_menu_handler_specs()
self.assertNotIn('wuttaweb.menus:MenuHandler', specs)
self.assertNotIn('tests.test_handler:MockMenuHandler', specs)
self.assertIn('tests.test_handler:LegacyMenuHandler', specs)
# nb. this remains from previous config default
self.assertIn('tests.test_handler:AnotherMenuHandler', specs)
def test_make_form(self):
handler = self.make_handler()