Compare commits
No commits in common. "2b3d69a3794f941e4c360304e4c5f84f2d97d841" and "4b7092bd0aec1e72c80e30a4b8c05bd73df778bb" have entirely different histories.
2b3d69a379
...
4b7092bd0a
|
@ -70,9 +70,6 @@ wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension"
|
||||||
[project.entry-points."wutta.typer_imports"]
|
[project.entry-points."wutta.typer_imports"]
|
||||||
wuttaweb = "wuttaweb.cli"
|
wuttaweb = "wuttaweb.cli"
|
||||||
|
|
||||||
[project.entry-points."wutta.web.menus"]
|
|
||||||
wuttaweb = "wuttaweb.menus:MenuHandler"
|
|
||||||
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://wuttaproject.org/"
|
Homepage = "https://wuttaproject.org/"
|
||||||
|
|
|
@ -465,33 +465,6 @@ class StringAlchemyFilter(AlchemyFilter):
|
||||||
sa.and_(*criteria)))
|
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):
|
class BooleanAlchemyFilter(AlchemyFilter):
|
||||||
"""
|
"""
|
||||||
SQLAlchemy filter option for a boolean data column.
|
SQLAlchemy filter option for a boolean data column.
|
||||||
|
@ -595,8 +568,6 @@ default_sqlalchemy_filters = {
|
||||||
None: AlchemyFilter,
|
None: AlchemyFilter,
|
||||||
sa.String: StringAlchemyFilter,
|
sa.String: StringAlchemyFilter,
|
||||||
sa.Text: StringAlchemyFilter,
|
sa.Text: StringAlchemyFilter,
|
||||||
sa.Numeric: NumericAlchemyFilter,
|
|
||||||
sa.Integer: IntegerAlchemyFilter,
|
|
||||||
sa.Boolean: BooleanAlchemyFilter,
|
sa.Boolean: BooleanAlchemyFilter,
|
||||||
sa.Date: DateAlchemyFilter,
|
sa.Date: DateAlchemyFilter,
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,7 @@
|
||||||
Web Handler
|
Web Handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from wuttjamaican.app import GenericHandler
|
from wuttjamaican.app import GenericHandler
|
||||||
from wuttjamaican.util import load_entry_points
|
|
||||||
|
|
||||||
from wuttaweb import static, forms, grids
|
from wuttaweb import static, forms, grids
|
||||||
|
|
||||||
|
@ -109,87 +106,22 @@ class WebHandler(GenericHandler):
|
||||||
|
|
||||||
def get_menu_handler(self, **kwargs):
|
def get_menu_handler(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get the configured :term:`menu handler` for the web app.
|
Get the configured "menu" handler for the web app.
|
||||||
|
|
||||||
Specify a custom handler in your config file like this:
|
Specify a custom handler in your config file like this:
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
[wutta.web]
|
[wutta.web]
|
||||||
menus.handler.spec = poser.web.menus:PoserMenuHandler
|
menus.handler_spec = poser.web.menus:PoserMenuHandler
|
||||||
|
|
||||||
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
|
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
|
||||||
"""
|
"""
|
||||||
spec = self.config.get(f'{self.appname}.web.menus.handler.spec')
|
if not hasattr(self, 'menu_handler'):
|
||||||
if not spec:
|
spec = self.config.get(f'{self.appname}.web.menus.handler_spec',
|
||||||
spec = self.config.get(f'{self.appname}.web.menus.handler_spec')
|
default='wuttaweb.menus:MenuHandler')
|
||||||
if spec:
|
self.menu_handler = self.app.load_object(spec)(self.config)
|
||||||
warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; "
|
return self.menu_handler
|
||||||
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):
|
def make_form(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -41,21 +41,6 @@
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">User/Auth</h3>
|
<h3 class="block is-size-3">User/Auth</h3>
|
||||||
|
@ -256,8 +241,6 @@
|
||||||
${parent.modify_vue_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
|
|
||||||
|
|
||||||
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
|
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
|
||||||
|
|
||||||
ThisPageData.editWebLibraryShowDialog = false
|
ThisPageData.editWebLibraryShowDialog = false
|
||||||
|
|
|
@ -1224,7 +1224,7 @@ class MasterView(View):
|
||||||
elif simple.get('type') is bool:
|
elif simple.get('type') is bool:
|
||||||
value = self.config.get_bool(name, default=simple.get('default', False))
|
value = self.config.get_bool(name, default=simple.get('default', False))
|
||||||
else:
|
else:
|
||||||
value = self.config.get(name, default=simple.get('default'))
|
value = self.config.get(name)
|
||||||
|
|
||||||
normalized[name] = value
|
normalized[name] = value
|
||||||
|
|
||||||
|
@ -2707,9 +2707,6 @@ class MasterView(View):
|
||||||
represents a Wutta-based SQLAlchemy model, the return value
|
represents a Wutta-based SQLAlchemy model, the return value
|
||||||
for this method is: ``('uuid',)``
|
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
|
But there is no "sane" default for other scenarios, in which
|
||||||
case subclass should define :attr:`model_key`. If the model
|
case subclass should define :attr:`model_key`. If the model
|
||||||
key cannot be determined, raises ``AttributeError``.
|
key cannot be determined, raises ``AttributeError``.
|
||||||
|
@ -2724,12 +2721,8 @@ class MasterView(View):
|
||||||
|
|
||||||
model_class = cls.get_model_class()
|
model_class = cls.get_model_class()
|
||||||
if model_class:
|
if model_class:
|
||||||
# nb. we want the primary key but must avoid column names
|
mapper = sa.inspect(model_class)
|
||||||
# in case mapped class uses different prop keys
|
return tuple([column.key for column in mapper.primary_key])
|
||||||
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}")
|
raise AttributeError(f"you must define model_key for view class: {cls}")
|
||||||
|
|
||||||
|
|
|
@ -129,10 +129,6 @@ class AppInfoView(MasterView):
|
||||||
{'name': f'{self.config.appname}.node_title'},
|
{'name': f'{self.config.appname}.node_title'},
|
||||||
{'name': f'{self.config.appname}.production',
|
{'name': f'{self.config.appname}.production',
|
||||||
'type': bool},
|
'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
|
# user/auth
|
||||||
{'name': 'wuttaweb.home_redirect_to_login',
|
{'name': 'wuttaweb.home_redirect_to_login',
|
||||||
|
@ -168,15 +164,11 @@ class AppInfoView(MasterView):
|
||||||
|
|
||||||
def configure_get_context(self, **kwargs):
|
def configure_get_context(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
|
|
||||||
|
# normal context
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
|
|
||||||
# add registered menu handlers
|
# we will add `weblibs` to context, based on config values
|
||||||
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()
|
weblibs = self.get_weblibs()
|
||||||
for key in weblibs:
|
for key in weblibs:
|
||||||
title = weblibs[key]
|
title = weblibs[key]
|
||||||
|
@ -200,8 +192,8 @@ class AppInfoView(MasterView):
|
||||||
'live_url': get_liburl(self.request, key,
|
'live_url': get_liburl(self.request, key,
|
||||||
prefix=self.weblib_config_prefix),
|
prefix=self.weblib_config_prefix),
|
||||||
}
|
}
|
||||||
context['weblibs'] = list(weblibs.values())
|
|
||||||
|
|
||||||
|
context['weblibs'] = list(weblibs.values())
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -326,30 +326,6 @@ class TestStringAlchemyFilter(WebTestCase):
|
||||||
self.assertEqual(filtered_query.count(), 6)
|
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):
|
class TestBooleanAlchemyFilter(WebTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from wuttaweb import handler as mod, static
|
from wuttaweb import handler as mod, static
|
||||||
from wuttaweb.forms import Form
|
from wuttaweb.forms import Form
|
||||||
from wuttaweb.grids import Grid
|
from wuttaweb.grids import Grid
|
||||||
|
@ -9,16 +7,6 @@ from wuttaweb.menus import MenuHandler
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class MockMenuHandler(MenuHandler):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class LegacyMenuHandler(MenuHandler):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnotherMenuHandler(MenuHandler):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestWebHandler(WebTestCase):
|
class TestWebHandler(WebTestCase):
|
||||||
|
|
||||||
def make_handler(self):
|
def make_handler(self):
|
||||||
|
@ -72,71 +60,10 @@ class TestWebHandler(WebTestCase):
|
||||||
url = handler.get_main_logo_url(self.request)
|
url = handler.get_main_logo_url(self.request)
|
||||||
self.assertEqual(url, '/testing/other.png')
|
self.assertEqual(url, '/testing/other.png')
|
||||||
|
|
||||||
def test_get_menu_handler(self):
|
def test_menu_handler_default(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
# built-in default
|
|
||||||
menus = handler.get_menu_handler()
|
menus = handler.get_menu_handler()
|
||||||
self.assertIsInstance(menus, MenuHandler)
|
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):
|
def test_make_form(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
Loading…
Reference in a new issue