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("