feat: add MasterView registry/discovery mechanism
and show related MasterView link buttons when viewing App Table also show some more info about model class when viewing table
This commit is contained in:
parent
0619f070c7
commit
484e1f810a
8 changed files with 225 additions and 16 deletions
|
|
@ -164,6 +164,11 @@ def make_pyramid_config(settings):
|
||||||
)
|
)
|
||||||
pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
|
pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
|
||||||
|
|
||||||
|
# add some more config magic
|
||||||
|
pyramid_config.add_directive(
|
||||||
|
"add_wutta_master_view", "wuttaweb.conf.add_master_view"
|
||||||
|
)
|
||||||
|
|
||||||
return pyramid_config
|
return pyramid_config
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ class WuttaWebConfigExtension(WuttaConfigExtension):
|
||||||
"""
|
"""
|
||||||
Config extension for WuttaWeb.
|
Config extension for WuttaWeb.
|
||||||
|
|
||||||
This sets the default plugin for SQLAlchemy-Continuum. Which is
|
This sets the default plugin used for SQLAlchemy-Continuum, to
|
||||||
|
:class:`~wuttaweb.db.continuum.WuttaWebContinuumPlugin`. Which is
|
||||||
only relevant if Wutta-Continuum is installed and enabled. For
|
only relevant if Wutta-Continuum is installed and enabled. For
|
||||||
more info see :doc:`wutta-continuum:index`.
|
more info see :doc:`wutta-continuum:index`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -44,3 +45,45 @@ class WuttaWebConfigExtension(WuttaConfigExtension):
|
||||||
"wutta_continuum.wutta_plugin_spec",
|
"wutta_continuum.wutta_plugin_spec",
|
||||||
"wuttaweb.db.continuum:WuttaWebContinuumPlugin",
|
"wuttaweb.db.continuum:WuttaWebContinuumPlugin",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_master_view(config, master):
|
||||||
|
"""
|
||||||
|
Pyramid directive to add the given ``MasterView`` subclass to the
|
||||||
|
app's registry.
|
||||||
|
|
||||||
|
This allows the app to dynamically present certain options for
|
||||||
|
admin features etc.
|
||||||
|
|
||||||
|
This is normally called automatically for all master views, within
|
||||||
|
the :meth:`~wuttaweb.views.master.MasterView.defaults()` method.
|
||||||
|
|
||||||
|
Should you need to call this yourself, do not call it directly but
|
||||||
|
instead make a similar call via the Pyramid config object::
|
||||||
|
|
||||||
|
pyramid_config.add_wutta_master_view(PoserWidgetView)
|
||||||
|
|
||||||
|
:param config: Reference to the Pyramid config object.
|
||||||
|
|
||||||
|
:param master: Reference to a
|
||||||
|
:class:`~wuttaweb.views.master.MasterView` subclass.
|
||||||
|
|
||||||
|
This function is involved in app startup; once that phase is
|
||||||
|
complete you can inspect the master views like so::
|
||||||
|
|
||||||
|
master_views = request.registry.settings["wuttaweb_master_views"]
|
||||||
|
|
||||||
|
# find master views for given model class
|
||||||
|
user_views = master_views.get(model.User, [])
|
||||||
|
|
||||||
|
# some master views are registered by model name instead (if no class)
|
||||||
|
email_views = master_views.get("email_setting", [])
|
||||||
|
"""
|
||||||
|
key = master.get_model_class() or master.get_model_name()
|
||||||
|
|
||||||
|
def action():
|
||||||
|
master_views = config.get_settings().get("wuttaweb_master_views", {})
|
||||||
|
master_views.setdefault(key, []).append(master)
|
||||||
|
config.add_settings({"wuttaweb_master_views": master_views})
|
||||||
|
|
||||||
|
config.action(None, action)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,9 @@ class WebTestCase(DataTestCase):
|
||||||
self.pyramid_config.add_directive(
|
self.pyramid_config.add_directive(
|
||||||
"add_wutta_permission", "wuttaweb.auth.add_permission"
|
"add_wutta_permission", "wuttaweb.auth.add_permission"
|
||||||
)
|
)
|
||||||
|
self.pyramid_config.add_directive(
|
||||||
|
"add_wutta_master_view", "wuttaweb.conf.add_master_view"
|
||||||
|
)
|
||||||
self.pyramid_config.add_subscriber(
|
self.pyramid_config.add_subscriber(
|
||||||
"wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender"
|
"wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3840,6 +3840,9 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
model_title = cls.get_model_title()
|
model_title = cls.get_model_title()
|
||||||
model_title_plural = cls.get_model_title_plural()
|
model_title_plural = cls.get_model_title_plural()
|
||||||
|
|
||||||
|
# add to master view registry
|
||||||
|
config.add_wutta_master_view(cls)
|
||||||
|
|
||||||
# permission group
|
# permission group
|
||||||
config.add_wutta_permission_group(
|
config.add_wutta_permission_group(
|
||||||
permission_prefix, model_title_plural, overwrite=False
|
permission_prefix, model_title_plural, overwrite=False
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class ReportView(MasterView): # pylint: disable=abstract-method
|
||||||
* ``/reports/XXX``
|
* ``/reports/XXX``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model_name = "report"
|
||||||
model_title = "Report"
|
model_title = "Report"
|
||||||
model_key = "report_key"
|
model_key = "report_key"
|
||||||
filterable = False
|
filterable = False
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ Table Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from alembic import command as alembic_command
|
from alembic import command as alembic_command
|
||||||
from sqlalchemy_utils import get_mapper
|
from sqlalchemy_utils import get_mapper
|
||||||
|
|
@ -44,11 +45,12 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Master view showing all tables in the :term:`app database`.
|
Master view showing all tables in the :term:`app database`.
|
||||||
|
|
||||||
Default route prefix is ``tables``.
|
Default route prefix is ``app_tables``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/tables/``
|
* ``/tables/app/``
|
||||||
|
* ``/tables/app/XXX``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=duplicate-code
|
# pylint: disable=duplicate-code
|
||||||
|
|
@ -68,6 +70,8 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"name": "Table Name",
|
"name": "Table Name",
|
||||||
|
"module_name": "Module",
|
||||||
|
"module_file": "File",
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
@ -81,7 +85,11 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
form_fields = [
|
form_fields = [
|
||||||
"name",
|
"name",
|
||||||
"schema",
|
"schema",
|
||||||
|
"model_name",
|
||||||
|
"description",
|
||||||
# "row_count",
|
# "row_count",
|
||||||
|
"module_name",
|
||||||
|
"module_file",
|
||||||
]
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
|
|
@ -101,6 +109,31 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def normalize_table(self, table): # pylint: disable=missing-function-docstring
|
||||||
|
record = {
|
||||||
|
"name": table.name,
|
||||||
|
"schema": table.schema or "",
|
||||||
|
# "row_count": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls = get_mapper(table).class_
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
record.update(
|
||||||
|
{
|
||||||
|
"model_class": cls,
|
||||||
|
"model_name": cls.__name__,
|
||||||
|
"model_name_dotted": f"{cls.__module__}.{cls.__name__}",
|
||||||
|
"description": (cls.__doc__ or "").strip(),
|
||||||
|
"module_name": cls.__module__,
|
||||||
|
"module_file": sys.modules[cls.__module__].__file__,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return record
|
||||||
|
|
||||||
def get_grid_data( # pylint: disable=empty-docstring
|
def get_grid_data( # pylint: disable=empty-docstring
|
||||||
self, columns=None, session=None
|
self, columns=None, session=None
|
||||||
):
|
):
|
||||||
|
|
@ -109,13 +142,7 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for table in model.Base.metadata.tables.values():
|
for table in model.Base.metadata.tables.values():
|
||||||
data.append(
|
data.append(self.normalize_table(table))
|
||||||
{
|
|
||||||
"name": table.name,
|
|
||||||
"schema": table.schema or "",
|
|
||||||
# "row_count": 42,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -143,12 +170,11 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
|
|
||||||
name = self.request.matchdict["name"]
|
name = self.request.matchdict["name"]
|
||||||
table = model.Base.metadata.tables[name]
|
table = model.Base.metadata.tables[name]
|
||||||
data = {
|
|
||||||
"name": table.name,
|
# nb. sometimes need the real table reference later when
|
||||||
"schema": table.schema or "",
|
# dealing with an instance view
|
||||||
# "row_count": 42,
|
data = self.normalize_table(table)
|
||||||
"table": table,
|
data["table"] = table
|
||||||
}
|
|
||||||
|
|
||||||
self.__dict__["_cached_instance"] = data
|
self.__dict__["_cached_instance"] = data
|
||||||
|
|
||||||
|
|
@ -158,6 +184,44 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
|
||||||
""" """
|
""" """
|
||||||
return instance["name"]
|
return instance["name"]
|
||||||
|
|
||||||
|
def configure_form(self, form): # pylint: disable=empty-docstring
|
||||||
|
""" """
|
||||||
|
f = form
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# description
|
||||||
|
f.set_widget("description", "notes")
|
||||||
|
|
||||||
|
def get_xref_buttons(self, obj):
|
||||||
|
"""
|
||||||
|
By default this returns a list of buttons for each
|
||||||
|
:class:`~wuttaweb.views.master.MasterView` subclass registered
|
||||||
|
in the app for the current table model.
|
||||||
|
|
||||||
|
See also parent method docs,
|
||||||
|
:meth:`~wuttaweb.views.master.MasterView.get_xref_buttons()`
|
||||||
|
"""
|
||||||
|
table = obj
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
# nb. we do not omit any buttons due to lack of permission
|
||||||
|
# here. all buttons are shown for anyone seeing this page.
|
||||||
|
# this is for sake of clarity so admin users are aware of what
|
||||||
|
# is *possible* within the app etc.
|
||||||
|
master_views = self.request.registry.settings.get("wuttaweb_master_views", {})
|
||||||
|
model_views = master_views.get(table["model_class"], [])
|
||||||
|
for view in model_views:
|
||||||
|
buttons.append(
|
||||||
|
self.make_button(
|
||||||
|
view.get_model_title_plural(),
|
||||||
|
primary=True,
|
||||||
|
url=self.request.route_url(view.get_route_prefix()),
|
||||||
|
icon_left="eye",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
|
||||||
def get_row_grid_data(self, obj): # pylint: disable=empty-docstring
|
def get_row_grid_data(self, obj): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
table = obj
|
table = obj
|
||||||
|
|
|
||||||
61
tests/test_conf.py
Normal file
61
tests/test_conf.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
from wuttjamaican.db.model import User
|
||||||
|
from wuttjamaican.testing import ConfigTestCase
|
||||||
|
|
||||||
|
from wuttaweb import conf as mod
|
||||||
|
from wuttaweb.testing import WebTestCase
|
||||||
|
from wuttaweb.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class TestWuttaWebConfigExtension(ConfigTestCase):
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
|
||||||
|
# continuum plugin not set yet (b/c config was not extended)
|
||||||
|
self.assertIsNone(self.config.get("wutta_continuum.wutta_plugin_spec"))
|
||||||
|
|
||||||
|
# so let's extend it
|
||||||
|
extension = mod.WuttaWebConfigExtension()
|
||||||
|
extension.configure(self.config)
|
||||||
|
self.assertEqual(
|
||||||
|
self.config.get("wutta_continuum.wutta_plugin_spec"),
|
||||||
|
"wuttaweb.db.continuum:WuttaWebContinuumPlugin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MasterWithClass(MasterView):
|
||||||
|
model_class = User
|
||||||
|
|
||||||
|
|
||||||
|
class MasterWithName(MasterView):
|
||||||
|
model_class = "Widget"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddMasterView(WebTestCase):
|
||||||
|
|
||||||
|
def test_master_with_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# nb. due to minimal test bootstrapping, no master views are
|
||||||
|
# registered by default at this point
|
||||||
|
self.assertNotIn("wuttaweb_master_views", self.request.registry.settings)
|
||||||
|
|
||||||
|
self.pyramid_config.add_wutta_master_view(MasterWithClass)
|
||||||
|
self.assertIn("wuttaweb_master_views", self.request.registry.settings)
|
||||||
|
master_views = self.request.registry.settings["wuttaweb_master_views"]
|
||||||
|
self.assertIn(model.User, master_views)
|
||||||
|
self.assertEqual(master_views[model.User], [MasterWithClass])
|
||||||
|
|
||||||
|
def test_master_with_name(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# nb. due to minimal test bootstrapping, no master views are
|
||||||
|
# registered by default at this point
|
||||||
|
self.assertNotIn("wuttaweb_master_views", self.request.registry.settings)
|
||||||
|
|
||||||
|
self.pyramid_config.add_wutta_master_view(MasterWithName)
|
||||||
|
self.assertIn("wuttaweb_master_views", self.request.registry.settings)
|
||||||
|
master_views = self.request.registry.settings["wuttaweb_master_views"]
|
||||||
|
self.assertIn("Widget", master_views)
|
||||||
|
self.assertEqual(master_views["Widget"], [MasterWithName])
|
||||||
|
|
@ -9,6 +9,7 @@ from wuttjamaican.db.conf import check_alembic_current, make_alembic_config
|
||||||
|
|
||||||
from wuttaweb.testing import WebTestCase
|
from wuttaweb.testing import WebTestCase
|
||||||
from wuttaweb.views import tables as mod
|
from wuttaweb.views import tables as mod
|
||||||
|
from wuttaweb.views.users import UserView
|
||||||
|
|
||||||
|
|
||||||
class TestAppTableView(WebTestCase):
|
class TestAppTableView(WebTestCase):
|
||||||
|
|
@ -75,6 +76,34 @@ version_locations = wuttjamaican.db:alembic/versions
|
||||||
table = {"name": "poser_foo"}
|
table = {"name": "poser_foo"}
|
||||||
self.assertEqual(view.get_instance_title(table), "poser_foo")
|
self.assertEqual(view.get_instance_title(table), "poser_foo")
|
||||||
|
|
||||||
|
def test_configure_form(self):
|
||||||
|
view = self.make_view()
|
||||||
|
table = {"name": "user", "description": "Represents a user"}
|
||||||
|
|
||||||
|
# no description widget by default
|
||||||
|
form = view.make_form(model_instance=table, fields=["name", "description"])
|
||||||
|
self.assertNotIn("description", form.widgets)
|
||||||
|
|
||||||
|
# but it gets added when configuring
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertIn("description", form.widgets)
|
||||||
|
|
||||||
|
def test_get_xref_buttons(self):
|
||||||
|
self.pyramid_config.add_route("users", "/users/")
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# nb. must add this first
|
||||||
|
self.pyramid_config.add_wutta_master_view(UserView)
|
||||||
|
|
||||||
|
# now xref button should work
|
||||||
|
table = {"name": "person", "model_class": model.User}
|
||||||
|
buttons = view.get_xref_buttons(table)
|
||||||
|
self.assertEqual(len(buttons), 1)
|
||||||
|
button = buttons[0]
|
||||||
|
self.assertIn("Users", button)
|
||||||
|
self.assertIn("http://example.com/users/", button)
|
||||||
|
|
||||||
def test_get_row_grid_data(self):
|
def test_get_row_grid_data(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue