diff --git a/.pylintrc b/.pylintrc
index 527cc44..5a30127 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -2,4 +2,7 @@
[MESSAGES CONTROL]
disable=fixme,
- duplicate-code,
+
+[SIMILARITIES]
+# nb. cuts out some noise for duplicate-code
+min-similarity-lines=5
diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py
index 0250bfb..6365aa8 100644
--- a/src/wuttaweb/forms/base.py
+++ b/src/wuttaweb/forms/base.py
@@ -37,7 +37,13 @@ from colanderalchemy import SQLAlchemySchemaNode
from pyramid.renderers import render
from webhelpers2.html import HTML
-from wuttaweb.util import FieldList, get_form_data, get_model_fields, make_json_safe
+from wuttaweb.util import (
+ FieldList,
+ get_form_data,
+ get_model_fields,
+ make_json_safe,
+ render_vue_finalize,
+)
log = logging.getLogger(__name__)
@@ -1095,12 +1101,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
The actual output may depend on various form attributes, in
particular :attr:`vue_tagname`.
"""
- set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
- make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
- return HTML.tag(
- "script",
- c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
- )
+ return render_vue_finalize(self.vue_tagname, self.vue_component)
def get_vue_model_data(self):
"""
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index 2779bef..9461b23 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -39,7 +39,12 @@ from pyramid.renderers import render
from webhelpers2.html import HTML
from wuttjamaican.db.util import UUID
-from wuttaweb.util import FieldList, get_model_fields, make_json_safe
+from wuttaweb.util import (
+ FieldList,
+ get_model_fields,
+ make_json_safe,
+ render_vue_finalize,
+)
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
@@ -2212,12 +2217,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
The actual output may depend on various grid attributes, in
particular :attr:`vue_tagname`.
"""
- set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
- make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
- return HTML.tag(
- "script",
- c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
- )
+ return render_vue_finalize(self.vue_tagname, self.vue_component)
def get_vue_columns(self):
"""
diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py
index 963c848..5492485 100644
--- a/src/wuttaweb/util.py
+++ b/src/wuttaweb/util.py
@@ -618,6 +618,71 @@ def make_json_safe(value, key=None, warn=True):
return value
+def render_vue_finalize(vue_tagname, vue_component):
+ """
+ Render the Vue "finalize" script for a form or grid component.
+
+ This is a convenience for shared logic; it returns e.g.:
+
+ .. code-block:: html
+
+
+ """
+ set_data = f"{vue_component}.data = function() {{ return {vue_component}Data }}"
+ make_component = f"Vue.component('{vue_tagname}', {vue_component})"
+ return HTML.tag(
+ "script",
+ c=["\n", HTML.literal(set_data), "\n", HTML.literal(make_component), "\n"],
+ )
+
+
+def make_users_grid(request, **kwargs):
+ """
+ Make and return a users (sub)grid.
+
+ This grid is shown for the Users field when viewing a Person or
+ Role, for instance. It is called by the following methods:
+
+ * :meth:`wuttaweb.views.people.PersonView.make_users_grid()`
+ * :meth:`wuttaweb.views.roles.RoleView.make_users_grid()`
+
+ :returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
+ instance.
+ """
+ config = request.wutta_config
+ app = config.get_app()
+ model = app.model
+ web = app.get_web_handler()
+
+ if "key" not in kwargs:
+ route_prefix = kwargs.pop("route_prefix")
+ kwargs["key"] = f"{route_prefix}.view.users"
+
+ kwargs.setdefault("model_class", model.User)
+ grid = web.make_grid(request, **kwargs)
+
+ if request.has_perm("users.view"):
+
+ def view_url(user, i): # pylint: disable=unused-argument
+ return request.route_url("users.view", uuid=user.uuid)
+
+ grid.add_action("view", icon="eye", url=view_url)
+ grid.set_link("person")
+ grid.set_link("username")
+
+ if request.has_perm("users.edit"):
+
+ def edit_url(user, i): # pylint: disable=unused-argument
+ return request.route_url("users.edit", uuid=user.uuid)
+
+ grid.add_action("edit", url=edit_url)
+
+ return grid
+
+
##############################
# theme functions
##############################
diff --git a/src/wuttaweb/views/batch.py b/src/wuttaweb/views/batch.py
index fd68e7e..79bea75 100644
--- a/src/wuttaweb/views/batch.py
+++ b/src/wuttaweb/views/batch.py
@@ -51,6 +51,8 @@ class BatchMasterView(MasterView):
from :meth:`get_batch_handler()`.
"""
+ executable = True
+
labels = {
"id": "Batch ID",
"status_code": "Status",
@@ -330,29 +332,22 @@ class BatchMasterView(MasterView):
raise RuntimeError("can't find the batch")
time.sleep(0.1)
- try:
- # populate the batch
- self.batch_handler.do_populate(batch, progress=progress)
- session.flush()
-
- except Exception as error: # pylint: disable=broad-exception-caught
- session.rollback()
+ def onerror():
log.warning(
"failed to populate %s: %s",
self.get_model_title(),
batch,
exc_info=True,
)
- if progress:
- progress.handle_error(error)
- else:
- session.commit()
- if progress:
- progress.handle_success()
-
- finally:
- session.close()
+ self.do_thread_body(
+ self.batch_handler.do_populate,
+ (batch,),
+ {"progress": progress},
+ onerror,
+ session=session,
+ progress=progress,
+ )
##############################
# execute methods
@@ -418,36 +413,3 @@ class BatchMasterView(MasterView):
):
""" """
return row.STATUS.get(value, value)
-
- ##############################
- # configuration
- ##############################
-
- @classmethod
- def defaults(cls, config): # pylint: disable=empty-docstring
- """ """
- cls._defaults(config)
- cls._batch_defaults(config)
-
- @classmethod
- def _batch_defaults(cls, config):
- route_prefix = cls.get_route_prefix()
- permission_prefix = cls.get_permission_prefix()
- model_title = cls.get_model_title()
- instance_url_prefix = cls.get_instance_url_prefix()
-
- # execute
- config.add_route(
- f"{route_prefix}.execute",
- f"{instance_url_prefix}/execute",
- request_method="POST",
- )
- config.add_view(
- cls,
- attr="execute",
- route_name=f"{route_prefix}.execute",
- permission=f"{permission_prefix}.execute",
- )
- config.add_wutta_permission(
- permission_prefix, f"{permission_prefix}.execute", f"Execute {model_title}"
- )
diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py
index d22707d..500a276 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -822,33 +822,25 @@ class MasterView(View): # pylint: disable=too-many-public-methods
self, query, progress=None
):
""" """
- model_title_plural = self.get_model_title_plural()
-
- # nb. use new session, separate from web transaction
session = self.app.make_session()
records = query.with_session(session).all()
- try:
- self.delete_bulk_action(records, progress=progress)
-
- except Exception as error: # pylint: disable=broad-exception-caught
- session.rollback()
+ def onerror():
log.warning(
"failed to delete %s results for %s",
len(records),
- model_title_plural,
+ self.get_model_title_plural(),
exc_info=True,
)
- if progress:
- progress.handle_error(error)
- else:
- session.commit()
- if progress:
- progress.handle_success()
-
- finally:
- session.close()
+ self.do_thread_body(
+ self.delete_bulk_action,
+ (records,),
+ {"progress": progress},
+ onerror,
+ session=session,
+ progress=progress,
+ )
def delete_bulk_action(self, data, progress=None):
"""
@@ -2485,6 +2477,60 @@ class MasterView(View): # pylint: disable=too-many-public-methods
session = session or self.Session()
session.add(obj)
+ def do_thread_body( # pylint: disable=too-many-arguments,too-many-positional-arguments
+ self, func, args, kwargs, onerror=None, session=None, progress=None
+ ):
+ """
+ Generic method to invoke for thread operations.
+
+ :param func: Callable which performs the actual logic. This
+ will be wrapped with a try/except statement for error
+ handling.
+
+ :param args: Tuple of positional arguments to pass to the
+ ``func`` callable.
+
+ :param kwargs: Dict of keyword arguments to pass to the
+ ``func`` callable.
+
+ :param onerror: Optional callback to invoke if ``func`` raises
+ an error. It should not expect any arguments.
+
+ :param session: Optional :term:`db session` in effect. Note
+ that if supplied, it will be *committed* (or rolled back on
+ error) and *closed* by this method. If you need more
+ specialized handling, do not use this method (or don't
+ specify the ``session``).
+
+ :param progress: Optional progress factory. If supplied, this
+ is assumed to be a
+ :class:`~wuttaweb.progress.SessionProgress` instance, and
+ it will be updated per success or failure of ``func``
+ invocation.
+ """
+ try:
+ func(*args, **kwargs)
+
+ except Exception as error: # pylint: disable=broad-exception-caught
+ if session:
+ session.rollback()
+ if onerror:
+ onerror()
+ else:
+ log.warning("failed to invoke thread callable: %s", func, exc_info=True)
+ if progress:
+ progress.handle_error(error)
+
+ else:
+ if session:
+ session.commit()
+ if progress:
+ progress.handle_success()
+
+ finally:
+ if session:
+ session.close()
+
##############################
# row methods
##############################
diff --git a/src/wuttaweb/views/people.py b/src/wuttaweb/views/people.py
index d832ed4..b726772 100644
--- a/src/wuttaweb/views/people.py
+++ b/src/wuttaweb/views/people.py
@@ -28,6 +28,7 @@ import sqlalchemy as sa
from wuttjamaican.db.model import Person
from wuttaweb.views import MasterView
+from wuttaweb.util import make_users_grid
class PersonView(MasterView): # pylint: disable=abstract-method
@@ -107,12 +108,9 @@ class PersonView(MasterView): # pylint: disable=abstract-method
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
- model = self.app.model
- route_prefix = self.get_route_prefix()
-
- grid = self.make_grid(
- key=f"{route_prefix}.view.users",
- model_class=model.User,
+ return make_users_grid(
+ self.request,
+ route_prefix=self.get_route_prefix(),
data=person.users,
columns=[
"username",
@@ -120,23 +118,6 @@ class PersonView(MasterView): # pylint: disable=abstract-method
],
)
- if self.request.has_perm("users.view"):
-
- def view_url(user, i): # pylint: disable=unused-argument
- return self.request.route_url("users.view", uuid=user.uuid)
-
- grid.add_action("view", icon="eye", url=view_url)
- grid.set_link("username")
-
- if self.request.has_perm("users.edit"):
-
- def edit_url(user, i): # pylint: disable=unused-argument
- return self.request.route_url("users.edit", uuid=user.uuid)
-
- grid.add_action("edit", url=edit_url)
-
- return grid
-
def objectify(self, form): # pylint: disable=empty-docstring
""" """
person = super().objectify(form)
diff --git a/src/wuttaweb/views/roles.py b/src/wuttaweb/views/roles.py
index 20299c3..f1f9b1e 100644
--- a/src/wuttaweb/views/roles.py
+++ b/src/wuttaweb/views/roles.py
@@ -29,6 +29,7 @@ from wuttaweb.views import MasterView
from wuttaweb.db import Session
from wuttaweb.forms import widgets
from wuttaweb.forms.schema import Permissions, RoleRef
+from wuttaweb.util import make_users_grid
class RoleView(MasterView): # pylint: disable=abstract-method
@@ -151,12 +152,9 @@ class RoleView(MasterView): # pylint: disable=abstract-method
:returns: Fully configured :class:`~wuttaweb.grids.base.Grid`
instance.
"""
- model = self.app.model
- route_prefix = self.get_route_prefix()
-
- grid = self.make_grid(
- key=f"{route_prefix}.view.users",
- model_class=model.User,
+ return make_users_grid(
+ self.request,
+ route_prefix=self.get_route_prefix(),
data=role.users,
columns=[
"username",
@@ -165,24 +163,6 @@ class RoleView(MasterView): # pylint: disable=abstract-method
],
)
- if self.request.has_perm("users.view"):
-
- def view_url(user, i): # pylint: disable=unused-argument
- return self.request.route_url("users.view", uuid=user.uuid)
-
- grid.add_action("view", icon="eye", url=view_url)
- grid.set_link("person")
- grid.set_link("username")
-
- if self.request.has_perm("users.edit"):
-
- def edit_url(user, i): # pylint: disable=unused-argument
- return self.request.route_url("users.edit", uuid=user.uuid)
-
- grid.add_action("edit", url=edit_url)
-
- return grid
-
def unique_name(self, node, value): # pylint: disable=empty-docstring
""" """
model = self.app.model
diff --git a/tests/test_util.py b/tests/test_util.py
index 48c9695..a0285a1 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -16,6 +16,7 @@ from wuttjamaican.util import resource_path
from wuttaweb import util as mod
from wuttaweb.app import establish_theme
+from wuttaweb.grids import Grid
from wuttaweb.testing import WebTestCase
@@ -665,6 +666,52 @@ class TestMakeJsonSafe(TestCase):
)
+class TestRenderVueFinalize(TestCase):
+
+ def basic(self):
+ html = mod.render_vue_finalize("wutta-grid", "WuttaGrid")
+ self.assertIn("