From 09612b1921af0a7b3bb7141381c3bb861b4d64ab Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 15 Aug 2024 23:46:58 -0500 Subject: [PATCH 001/102] fix: fix some more wutta compat for base template missed those earlier --- tailbone/templates/base.mako | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 27e900e4..3a12859e 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -280,7 +280,7 @@ ${index_title} - % if master.creatable and master.show_create_link and master.has_perm('create'): + % if master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'): ${h.link_to(instance_title, instance_url)} - % elif master.creatable and master.show_create_link and master.has_perm('create'): + % elif master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'): % if not request.matched_route.name.endswith('.create'): Date: Fri, 16 Aug 2024 11:56:12 -0500 Subject: [PATCH 002/102] feat: inherit most logic from wuttaweb, for GridAction --- tailbone/grids/core.py | 65 ++++++++++---------------- tailbone/templates/grids/b-table.mako | 11 ++--- tailbone/templates/grids/complete.mako | 8 +--- tailbone/views/master.py | 8 +++- tailbone/views/people.py | 2 +- tailbone/views/purchasing/receiving.py | 4 +- tailbone/views/roles.py | 2 +- tests/grids/test_core.py | 17 +++++++ tests/views/test_master.py | 9 ++++ 9 files changed, 65 insertions(+), 61 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 3f1769cf..b9254c18 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -38,6 +38,7 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage +from wuttaweb.grids import GridAction as WuttaGridAction from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -1801,18 +1802,20 @@ class Grid: return False -class GridAction(object): +class GridAction(WuttaGridAction): """ - Represents an action available to a grid. This is used to construct the - 'actions' column when rendering the grid. + Represents a "row action" hyperlink within a grid context. - :param key: Key for the action (e.g. ``'edit'``), unique within - the grid. + This is a subclass of + :class:`wuttaweb:wuttaweb.grids.base.GridAction`. - :param label: Label to be displayed for the action. If not set, - will be a capitalized version of ``key``. + .. warning:: - :param icon: Icon name for the action. + This class remains for now, to retain compatibility with + existing code. But at some point the WuttaWeb class will + supersede this one entirely. + + :param target: HTML "target" attribute for the ```` tag. :param click_handler: Optional JS click handler for the action. This value will be rendered as-is within the final grid @@ -1824,41 +1827,23 @@ class GridAction(object): * ``$emit('do-something', props.row)`` """ - def __init__(self, key, label=None, url='#', icon=None, target=None, - link_class=None, click_handler=None): - self.key = key - self.label = label or prettify(key) - self.icon = icon - self.url = url + def __init__( + self, + request, + key, + target=None, + click_handler=None, + **kwargs, + ): + # TODO: previously url default was '#' - but i don't think we + # need that anymore? guess we'll see.. + #kwargs.setdefault('url', '#') + + super().__init__(request, key, **kwargs) + self.target = target - self.link_class = link_class self.click_handler = click_handler - def get_url(self, row, i): - """ - Returns an action URL for the given row. - """ - if callable(self.url): - return self.url(row, i) - return self.url - - def render_icon(self): - """ - Render the HTML snippet for the action link icon. - """ - return HTML.tag('i', class_='fas fa-{}'.format(self.icon)) - - def render_label(self): - """ - Render the label "text" within the actions column of a grid - row. Most actions have a static label that never varies, but - you can override this to add e.g. HTML content. Note that the - return value will be treated / rendered as HTML whether or not - it contains any, so perhaps be careful that it is trusted - content. - """ - return self.label - class URLMaker(object): """ diff --git a/tailbone/templates/grids/b-table.mako b/tailbone/templates/grids/b-table.mako index 632193b5..da9f2aae 100644 --- a/tailbone/templates/grids/b-table.mako +++ b/tailbone/templates/grids/b-table.mako @@ -53,11 +53,11 @@ % endfor - % if grid.main_actions or grid.more_actions: + % if grid.actions: <${b}-table-column field="actions" label="Actions" v-slot="props"> - % for action in grid.main_actions: + % for action in grid.actions: - % if request.use_oruga: - - % else: - - % endif - ${action.label} + ${action.render_icon_and_label()}   % endfor diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index fc48916b..93bb6c26 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -163,13 +163,7 @@ target="${action.target}" % endif > - % if request.use_oruga: - - ${action.render_label()|n} - % else: - ${action.render_icon()|n} - ${action.render_label()|n} - % endif + ${action.render_icon_and_label()}   % endfor diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 0d322da3..097cb229 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -3220,14 +3220,18 @@ class MasterView(View): def make_action(self, key, url=None, factory=None, **kwargs): """ - Make a new :class:`GridAction` instance for the current grid. + Make and return a new :class:`~tailbone.grids.core.GridAction` + instance. + + This can be called to make actions for any grid, not just the + one from :meth:`index()`. """ if url is None: route = '{}.{}'.format(self.get_route_prefix(), key) url = lambda r, i: self.request.route_url(route, **self.get_action_route_kwargs(r)) if not factory: factory = grids.GridAction - return factory(key, url=url, **kwargs) + return factory(self.request, key, url=url, **kwargs) def get_action_route_kwargs(self, obj): """ diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 94c85821..163a9a52 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -552,7 +552,7 @@ class PersonView(MasterView): if self.request.has_perm('trainwreck.transactions.view'): url = lambda row, i: self.request.route_url('trainwreck.transactions.view', uuid=row.uuid) - g.main_actions.append(grids.GridAction('view', icon='eye', url=url)) + g.main_actions.append(self.make_action('view', icon='eye', url=url)) g.load_settings() g.set_enum('system', self.enum.TRAINWRECK_SYSTEM) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 55936184..0a305f0a 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -40,7 +40,7 @@ from webhelpers2.html import tags, HTML from wuttaweb.util import get_form_data -from tailbone import forms, grids +from tailbone import forms from tailbone.views.purchasing import PurchasingBatchView @@ -1031,7 +1031,7 @@ class ReceivingBatchView(PurchasingBatchView): if batch.is_truck_dump_parent(): permission_prefix = self.get_permission_prefix() if self.request.has_perm('{}.edit_row'.format(permission_prefix)): - transform = grids.GridAction('transform', + transform = self.make_action('transform', icon='shuffle', label="Transform to Unit", url=self.transform_unit_url) diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index b34b3673..fb834479 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -363,7 +363,7 @@ class RoleView(PrincipalMasterView): if role.users: users = sorted(role.users, key=lambda u: u.username) actions = [ - grids.GridAction('view', icon='zoomin', + self.make_action('view', icon='zoomin', url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)) ] kwargs['users'] = grids.Grid(None, users, ['username', 'active'], diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index e6f9d675..0a8d5d66 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -137,3 +137,20 @@ class TestGrid(WebTestCase): # calling again returns same data data2 = grid.get_vue_data() self.assertIs(data2, data) + + +class TestGridAction(WebTestCase): + + def test_constructor(self): + + # null by default + action = mod.GridAction(self.request, 'view') + self.assertIsNone(action.target) + self.assertIsNone(action.click_handler) + + # but can set them + action = mod.GridAction(self.request, 'view', + target='_blank', + click_handler='doSomething(props.row)') + self.assertEqual(action.target, '_blank') + self.assertEqual(action.click_handler, 'doSomething(props.row)') diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 19321496..572875a0 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -3,6 +3,7 @@ from unittest.mock import patch from tailbone.views import master as mod +from wuttaweb.grids import GridAction from tests.util import WebTestCase @@ -24,3 +25,11 @@ class TestMasterView(WebTestCase): # sanity / coverage check kw = view.make_form_kwargs(model_instance=setting) self.assertIsNotNone(kw['action_url']) + + def test_make_action(self): + model = self.app.model + with patch.multiple(mod.MasterView, create=True, + model_class=model.Setting): + view = self.make_view() + action = view.make_action('view') + self.assertIsInstance(action, GridAction) From f7641218cb44c6ad18d6672361d1f1243c05e397 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 11:56:54 -0500 Subject: [PATCH 003/102] fix: avoid route error in user view, when using wutta people view kind of a temporary edge case here, can eventually change it back --- tailbone/views/users.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index f8bcb1b8..9eae74d8 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -208,9 +208,13 @@ class UserView(PrincipalMasterView): person_display = str(person) elif self.editing: person_display = str(user.person or '') - people_url = self.request.route_url('people.autocomplete') - f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget( - field_display=person_display, service_url=people_url)) + try: + people_url = self.request.route_url('people.autocomplete') + except KeyError: + pass # TODO: wutta compat + else: + f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget( + field_display=person_display, service_url=people_url)) f.set_validator('person_uuid', self.valid_person) f.set_label('person_uuid', "Person") From 2a0b6da2f9169c22c099ca2c367a3ab2d89fa6e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 14:34:50 -0500 Subject: [PATCH 004/102] feat: inherit from wutta base class for Grid --- tailbone/grids/core.py | 241 ++++++++++--------------- tailbone/views/batch/core.py | 8 +- tailbone/views/batch/pos.py | 1 + tailbone/views/customers.py | 19 +- tailbone/views/custorders/items.py | 1 + tailbone/views/custorders/orders.py | 71 ++++---- tailbone/views/departments.py | 8 +- tailbone/views/email.py | 2 +- tailbone/views/employees.py | 3 +- tailbone/views/master.py | 48 +++-- tailbone/views/members.py | 3 +- tailbone/views/people.py | 19 +- tailbone/views/poser/reports.py | 2 +- tailbone/views/principal.py | 6 +- tailbone/views/products.py | 28 +-- tailbone/views/purchasing/batch.py | 4 +- tailbone/views/purchasing/receiving.py | 18 +- tailbone/views/reports.py | 12 +- tailbone/views/roles.py | 15 +- tailbone/views/tempmon/core.py | 6 +- tailbone/views/trainwreck/base.py | 12 +- tailbone/views/users.py | 15 +- tests/grids/test_core.py | 49 ++++- 23 files changed, 317 insertions(+), 274 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index b9254c18..a5617215 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -38,7 +38,7 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage -from wuttaweb.grids import GridAction as WuttaGridAction +from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -61,7 +61,7 @@ class FieldList(list): self.insert(i + 1, newfield) -class Grid: +class Grid(WuttaGrid): """ Core grid class. In sore need of documentation. @@ -186,32 +186,59 @@ class Grid: grid.row_uuid_getter = fake_uuid """ - def __init__(self, key, data, columns=None, width='auto', request=None, - model_class=None, model_title=None, model_title_plural=None, - enums={}, labels={}, assume_local_times=False, renderers={}, invisible=[], - raw_renderers={}, - extra_row_class=None, linked_columns=[], url='#', - joiners={}, filterable=False, filters={}, use_byte_string_filters=False, - searchable={}, - sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', - pageable=False, default_pagesize=None, default_page=1, - checkboxes=False, checked=None, check_handler=None, check_all_handler=None, - checkable=None, row_uuid_getter=None, - clicking_row_checks_box=False, click_handlers=None, - main_actions=[], more_actions=[], delete_speedbump=False, - ajax_data_url=None, - vue_tagname=None, - expose_direct_link=False, - **kwargs): + def __init__( + self, + request, + key=None, + data=None, + width='auto', + model_title=None, + model_title_plural=None, + enums={}, + assume_local_times=False, + invisible=[], + raw_renderers={}, + extra_row_class=None, + url='#', + joiners={}, + filterable=False, + filters={}, + use_byte_string_filters=False, + searchable={}, + sortable=False, + sorters={}, + default_sortkey=None, + default_sortdir='asc', + pageable=False, + default_pagesize=None, + default_page=1, + checkboxes=False, + checked=None, + check_handler=None, + check_all_handler=None, + checkable=None, + row_uuid_getter=None, + clicking_row_checks_box=False, + click_handlers=None, + main_actions=[], + more_actions=[], + delete_speedbump=False, + ajax_data_url=None, + expose_direct_link=False, + **kwargs, + ): + if kwargs.get('component'): + warnings.warn("component param is deprecated for Grid(); " + "please use vue_tagname param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('vue_tagname', kwargs.pop('component')) - self.key = key - self.data = data - self.columns = FieldList(columns) if columns is not None else None - self.width = width - self.request = request - self.model_class = model_class - if self.model_class and self.columns is None: - self.columns = self.make_columns() + # TODO: pretty sure this should go away? + kwargs.setdefault('vue_tagname', 'tailbone-grid') + + kwargs['key'] = key + kwargs['data'] = data + super().__init__(request, **kwargs) self.model_title = model_title if not self.model_title and self.model_class and hasattr(self.model_class, 'get_model_title'): @@ -224,15 +251,13 @@ class Grid: if not self.model_title_plural: self.model_title_plural = '{}s'.format(self.model_title) + self.width = width self.enums = enums or {} - - self.labels = labels or {} self.assume_local_times = assume_local_times - self.renderers = self.make_default_renderers(renderers or {}) + self.renderers = self.make_default_renderers(self.renderers) self.raw_renderers = raw_renderers or {} self.invisible = invisible or [] self.extra_row_class = extra_row_class - self.linked_columns = linked_columns or [] self.url = url self.joiners = joiners or {} @@ -263,8 +288,6 @@ class Grid: self.click_handlers = click_handlers or {} - self.main_actions = main_actions or [] - self.more_actions = more_actions or [] self.delete_speedbump = delete_speedbump if ajax_data_url: @@ -274,29 +297,22 @@ class Grid: else: self.ajax_data_url = '' - # vue_tagname - self.vue_tagname = vue_tagname - if not self.vue_tagname and kwargs.get('component'): - warnings.warn("component kwarg is deprecated for Grid(); " - "please use vue_tagname param instead", + self.main_actions = main_actions or [] + if self.main_actions: + warnings.warn("main_actions param is deprecated for Grdi(); " + "please use actions param instead", DeprecationWarning, stacklevel=2) - self.vue_tagname = kwargs['component'] - if not self.vue_tagname: - self.vue_tagname = 'tailbone-grid' + self.actions.extend(self.main_actions) + self.more_actions = more_actions or [] + if self.more_actions: + warnings.warn("more_actions param is deprecated for Grdi(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + self.actions.extend(self.more_actions) self.expose_direct_link = expose_direct_link self._whgrid_kwargs = kwargs - @property - def vue_component(self): - """ - String name for the Vue component, e.g. ``'TailboneGrid'``. - - This is a generated value based on :attr:`vue_tagname`. - """ - words = self.vue_tagname.split('-') - return ''.join([word.capitalize() for word in words]) - @property def component(self): """ @@ -317,34 +333,6 @@ class Grid: DeprecationWarning, stacklevel=2) return self.vue_component - @property - def actions(self): - """ """ - actions = [] - if self.main_actions: - actions.extend(self.main_actions) - if self.more_actions: - actions.extend(self.more_actions) - return actions - - def make_columns(self): - """ - Return a default list of columns, based on :attr:`model_class`. - """ - if not self.model_class: - raise ValueError("Must define model_class to use make_columns()") - - mapper = orm.class_mapper(self.model_class) - return [prop.key for prop in mapper.iterate_properties] - - def remove(self, *keys): - """ - This *removes* some column(s) from the grid, altogether. - """ - for key in keys: - if key in self.columns: - self.columns.remove(key) - def hide_column(self, key): """ This *removes* a column from the grid, altogether. @@ -377,9 +365,6 @@ class Grid: if key in self.invisible: self.invisible.remove(key) - def append(self, field): - self.columns.append(field) - def insert_before(self, field, newfield): self.columns.insert_before(field, newfield) @@ -430,24 +415,22 @@ class Grid: self.filters.pop(key, None) def set_label(self, key, label, column_only=False): - self.labels[key] = label + """ + Set/override the label for a column. + + This overrides + :meth:`~wuttaweb:wuttaweb.grids.base.Grid.set_label()` to add + the following params: + + :param column_only: Boolean indicating whether the label + should be applied *only* to the column header (if + ``True``), vs. applying also to the filter (if ``False``). + """ + super().set_label(key, label) + if not column_only and key in self.filters: self.filters[key].label = label - def get_label(self, key): - """ - Returns the label text for given field key. - """ - return self.labels.get(key, prettify(key)) - - def set_link(self, key, link=True): - if link: - if key not in self.linked_columns: - self.linked_columns.append(key) - else: # unlink - if self.linked_columns and key in self.linked_columns: - self.linked_columns.remove(key) - def set_click_handler(self, key, handler): if handler: self.click_handlers[key] = handler @@ -457,9 +440,6 @@ class Grid: def has_click_handler(self, key): return key in self.click_handlers - def set_renderer(self, key, renderer): - self.renderers[key] = renderer - def set_raw_renderer(self, key, renderer): """ Set or remove the "raw" renderer for the given field. @@ -1450,22 +1430,13 @@ class Grid: return render(template, context) def get_view_click_handler(self): - + """ """ # locate the 'view' action # TODO: this should be easier, and/or moved elsewhere? view = None - for action in self.main_actions: + for action in self.actions: if action.key == 'view': - view = action - break - if not view: - for action in self.more_actions: - if action.key == 'view': - view = action - break - - if view: - return view.click_handler + return action.click_handler def set_filters_sequence(self, filters, only=False): """ @@ -1561,26 +1532,21 @@ class Grid: kwargs['form'] = form return render(template, kwargs) - def render_actions(self, row, i): - """ - Returns the rendered contents of the 'actions' column for a given row. - """ - main_actions = [self.render_action(a, row, i) - for a in self.main_actions] - main_actions = [a for a in main_actions if a] - more_actions = [self.render_action(a, row, i) - for a in self.more_actions] - more_actions = [a for a in more_actions if a] - if more_actions: - icon = HTML.tag('span', class_='ui-icon ui-icon-carat-1-e') - link = tags.link_to("More" + icon, '#', class_='more') - main_actions.append(HTML.literal('  ') + link + HTML.tag('div', class_='more', c=more_actions)) - return HTML.literal('').join(main_actions) + def render_actions(self, row, i): # pragma: no cover + """ """ + warnings.warn("grid.render_actions() is deprecated!", + DeprecationWarning, stacklevel=2) + + actions = [self.render_action(a, row, i) + for a in self.actions] + actions = [a for a in actions if a] + return HTML.literal('').join(actions) + + def render_action(self, action, row, i): # pragma: no cover + """ """ + warnings.warn("grid.render_action() is deprecated!", + DeprecationWarning, stacklevel=2) - def render_action(self, action, row, i): - """ - Renders an action menu item (link) for the given row. - """ url = action.get_url(row, i) if url: kwargs = {'class_': action.key, 'target': action.target} @@ -1786,21 +1752,10 @@ class Grid: Pre-generate all action URLs for the given data row. Meant for use with client-side table, since we can't generate URLs from JS. """ - for action in (self.main_actions + self.more_actions): + for action in self.actions: url = action.get_url(rowobj, i) row['_action_url_{}'.format(action.key)] = url - def is_linked(self, name): - """ - Should return ``True`` if the given column name is configured to be - "linked" (i.e. table cell should contain a link to "view object"), - otherwise ``False``. - """ - if self.linked_columns: - if name in self.linked_columns: - return True - return False - class GridAction(WuttaGridAction): """ diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index f4f74a34..5dd7b548 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -186,7 +186,9 @@ class BatchMasterView(MasterView): breakdown = self.make_status_breakdown(batch) factory = self.get_grid_factory() - g = factory('batch_row_status_breakdown', [], + g = factory(self.request, + key='batch_row_status_breakdown', + data=[], columns=['title', 'count']) g.set_click_handler('title', "autoFilterStatus(props.row)") kwargs['status_breakdown_data'] = breakdown @@ -693,7 +695,7 @@ class BatchMasterView(MasterView): batch = self.get_instance() # TODO: most of this logic is copied from MasterView, should refactor/merge somehow... - if 'main_actions' not in kwargs: + if 'actions' not in kwargs: actions = [] # view action @@ -714,7 +716,7 @@ class BatchMasterView(MasterView): actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url)) kwargs.setdefault('delete_speedbump', self.rows_deletable_speedbump) - kwargs['main_actions'] = actions + kwargs['actions'] = actions return super().make_row_grid_kwargs(**kwargs) diff --git a/tailbone/views/batch/pos.py b/tailbone/views/batch/pos.py index 11031353..b6fef6c8 100644 --- a/tailbone/views/batch/pos.py +++ b/tailbone/views/batch/pos.py @@ -195,6 +195,7 @@ class POSBatchView(BatchMasterView): factory = self.get_grid_factory() g = factory( + self.request, key=f'{route_prefix}.taxes', data=[], columns=[ diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index 2958a98a..7e49ccef 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -208,8 +208,7 @@ class CustomerView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) g.set_link('name') g.set_link('person') @@ -471,7 +470,8 @@ class CustomerView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.people'.format(route_prefix), + self.request, + key=f'{route_prefix}.people', data=[], columns=[ 'shopper_number', @@ -500,7 +500,8 @@ class CustomerView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.people'.format(route_prefix), + self.request, + key=f'{route_prefix}.people', data=[], columns=[ 'full_name', @@ -512,13 +513,13 @@ class CustomerView(MasterView): ) if self.request.has_perm('people.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('people.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) if self.people_detachable and self.has_perm('detach_person'): - g.main_actions.append(self.make_action('detach', icon='minus-circle', - link_class='has-text-warning', - click_handler="$emit('detach-person', props.row._action_url_detach)")) + g.actions.append(self.make_action('detach', icon='minus-circle', + link_class='has-text-warning', + click_handler="$emit('detach-person', props.row._action_url_detach)")) return HTML.literal( g.render_table_element(data_prop='peopleData')) diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index d8e39f55..e7edf3aa 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -385,6 +385,7 @@ class CustomerOrderItemView(MasterView): factory = self.get_grid_factory() g = factory( + self.request, key=f'{route_prefix}.events', data=[], columns=[ diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index f76d4d93..b1a9831a 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -29,13 +29,12 @@ import logging from sqlalchemy import orm -from rattail.db import model -from rattail.util import pretty_quantity, simple_error +from rattail.db.model import CustomerOrder, CustomerOrderItem +from rattail.util import simple_error from rattail.batch import get_batch_handler from webhelpers2.html import tags, HTML -from tailbone.db import Session from tailbone.views import MasterView @@ -46,7 +45,7 @@ class CustomerOrderView(MasterView): """ Master view for customer orders """ - model_class = model.CustomerOrder + model_class = CustomerOrder route_prefix = 'custorders' editable = False configurable = True @@ -80,7 +79,7 @@ class CustomerOrderView(MasterView): ] has_rows = True - model_row_class = model.CustomerOrderItem + model_row_class = CustomerOrderItem rows_viewable = False row_labels = { @@ -116,15 +115,17 @@ class CustomerOrderView(MasterView): ] def __init__(self, request): - super(CustomerOrderView, self).__init__(request) + super().__init__(request) self.batch_handler = self.get_batch_handler() def query(self, session): + model = self.app.model return session.query(model.CustomerOrder)\ .options(orm.joinedload(model.CustomerOrder.customer)) def configure_grid(self, g): super().configure_grid(g) + model = self.app.model # id g.set_link('id') @@ -163,7 +164,7 @@ class CustomerOrderView(MasterView): return f"#{order.id} for {order.customer or order.person}" def configure_form(self, f): - super(CustomerOrderView, self).configure_form(f) + super().configure_form(f) order = f.model_instance f.set_readonly('id') @@ -233,6 +234,7 @@ class CustomerOrderView(MasterView): class_='has-background-warning') def get_row_data(self, order): + model = self.app.model return self.Session.query(model.CustomerOrderItem)\ .filter(model.CustomerOrderItem.order == order) @@ -240,11 +242,13 @@ class CustomerOrderView(MasterView): return item.order def make_row_grid_kwargs(self, **kwargs): - kwargs = super(CustomerOrderView, self).make_row_grid_kwargs(**kwargs) + kwargs = super().make_row_grid_kwargs(**kwargs) - assert not kwargs['main_actions'] - kwargs['main_actions'].append( - self.make_action('view', icon='eye', url=self.row_view_action_url)) + actions = kwargs.get('actions', []) + if not actions: + actions.append(self.make_action('view', icon='eye', + url=self.row_view_action_url)) + kwargs['actions'] = actions return kwargs @@ -253,7 +257,7 @@ class CustomerOrderView(MasterView): return self.request.route_url('custorders.items.view', uuid=item.uuid) def configure_row_grid(self, g): - super(CustomerOrderView, self).configure_row_grid(g) + super().configure_row_grid(g) app = self.get_rattail_app() handler = app.get_batch_handler( 'custorder', @@ -423,6 +427,7 @@ class CustomerOrderView(MasterView): if not user: raise RuntimeError("this feature requires a user to be logged in") + model = self.app.model try: # there should be at most *one* new batch per user batch = self.Session.query(model.CustomerOrderBatch)\ @@ -488,6 +493,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a customer UUID"} + model = self.app.model customer = self.Session.get(model.Customer, uuid) if not customer: return {'error': "Customer not found"} @@ -508,6 +514,7 @@ class CustomerOrderView(MasterView): return info def assign_contact(self, batch, data): + model = self.app.model kwargs = {} # this will either be a Person or Customer UUID @@ -662,6 +669,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a product UUID"} + model = self.app.model product = self.Session.get(model.Product, uuid) if not product: return {'error': "Product not found"} @@ -725,8 +733,7 @@ class CustomerOrderView(MasterView): return app.render_currency(obj.unit_price) def normalize_row(self, row): - app = self.get_rattail_app() - products_handler = app.get_products_handler() + products_handler = self.app.get_products_handler() data = { 'uuid': row.uuid, @@ -742,20 +749,20 @@ class CustomerOrderView(MasterView): 'product_size': row.product_size, 'product_weighed': row.product_weighed, - 'case_quantity': pretty_quantity(row.case_quantity), - 'cases_ordered': pretty_quantity(row.cases_ordered), - 'units_ordered': pretty_quantity(row.units_ordered), - 'order_quantity': pretty_quantity(row.order_quantity), + 'case_quantity': self.app.render_quantity(row.case_quantity), + 'cases_ordered': self.app.render_quantity(row.cases_ordered), + 'units_ordered': self.app.render_quantity(row.units_ordered), + 'order_quantity': self.app.render_quantity(row.order_quantity), 'order_uom': row.order_uom, 'order_uom_choices': self.uom_choices_for_row(row), - 'discount_percent': pretty_quantity(row.discount_percent), + 'discount_percent': self.app.render_quantity(row.discount_percent), 'department_display': row.department_name, 'unit_price': float(row.unit_price) if row.unit_price is not None else None, 'unit_price_display': self.get_unit_price_display(row), 'total_price': float(row.total_price) if row.total_price is not None else None, - 'total_price_display': app.render_currency(row.total_price), + 'total_price_display': self.app.render_currency(row.total_price), 'status_code': row.status_code, 'status_text': row.status_text, @@ -763,15 +770,15 @@ class CustomerOrderView(MasterView): if row.unit_regular_price: data['unit_regular_price'] = float(row.unit_regular_price) - data['unit_regular_price_display'] = app.render_currency(row.unit_regular_price) + data['unit_regular_price_display'] = self.app.render_currency(row.unit_regular_price) if row.unit_sale_price: data['unit_sale_price'] = float(row.unit_sale_price) - data['unit_sale_price_display'] = app.render_currency(row.unit_sale_price) + data['unit_sale_price_display'] = self.app.render_currency(row.unit_sale_price) if row.sale_ends: - sale_ends = app.localtime(row.sale_ends, from_utc=True).date() + sale_ends = self.app.localtime(row.sale_ends, from_utc=True).date() data['sale_ends'] = str(sale_ends) - data['sale_ends_display'] = app.render_date(sale_ends) + data['sale_ends_display'] = self.app.render_date(sale_ends) if row.unit_sale_price and row.unit_price == row.unit_sale_price: data['pricing_reflects_sale'] = True @@ -808,12 +815,12 @@ class CustomerOrderView(MasterView): case_price = self.batch_handler.get_case_price_for_row(row) data['case_price'] = float(case_price) if case_price is not None else None - data['case_price_display'] = app.render_currency(case_price) + data['case_price_display'] = self.app.render_currency(case_price) if self.batch_handler.product_price_may_be_questionable(): data['price_needs_confirmation'] = row.price_needs_confirmation - key = app.get_product_key_field() + key = self.app.get_product_key_field() if key == 'upc': data['product_key'] = data['product_upc_pretty'] elif key == 'item_id': @@ -837,7 +844,7 @@ class CustomerOrderView(MasterView): case_qty = unit_qty = '??' else: case_qty = data['case_quantity'] - unit_qty = pretty_quantity(row.order_quantity * row.case_quantity) + unit_qty = self.app.render_quantity(row.order_quantity * row.case_quantity) data.update({ 'order_quantity_display': "{} {} (× {} {} = {} {})".format( data['order_quantity'], @@ -850,14 +857,14 @@ class CustomerOrderView(MasterView): else: data.update({ 'order_quantity_display': "{} {}".format( - pretty_quantity(row.order_quantity), + self.app.render_quantity(row.order_quantity), self.enum.UNIT_OF_MEASURE[unit_uom]), }) return data def add_item(self, batch, data): - app = self.get_rattail_app() + model = self.app.model order_quantity = decimal.Decimal(data.get('order_quantity') or '0') order_uom = data.get('order_uom') @@ -888,7 +895,7 @@ class CustomerOrderView(MasterView): pending_info = dict(data['pending_product']) if 'upc' in pending_info: - pending_info['upc'] = app.make_gpc(pending_info['upc']) + pending_info['upc'] = self.app.make_gpc(pending_info['upc']) for field in ('unit_cost', 'regular_price_amount', 'case_size'): if field in pending_info: @@ -917,6 +924,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a row UUID"} + model = self.app.model row = self.Session.get(model.CustomerOrderBatchRow, uuid) if not row: return {'error': "Row not found"} @@ -975,6 +983,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a row UUID"} + model = self.app.model row = self.Session.get(model.CustomerOrderBatchRow, uuid) if not row: return {'error': "Row not found"} diff --git a/tailbone/views/departments.py b/tailbone/views/departments.py index 6ee1439f..47de8dca 100644 --- a/tailbone/views/departments.py +++ b/tailbone/views/departments.py @@ -128,8 +128,8 @@ class DepartmentView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.employees'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.employees', data=[], columns=[ 'first_name', @@ -140,9 +140,9 @@ class DepartmentView(MasterView): ) if self.request.has_perm('employees.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('employees.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) return HTML.literal( g.render_table_element(data_prop='employeesData')) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 4014c05e..a99e8553 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -141,7 +141,7 @@ class EmailSettingView(MasterView): # toggle hidden if self.has_perm('configure'): - g.main_actions.append( + g.actions.append( self.make_action('toggle_hidden', url='#', icon='ban', click_handler='toggleHidden(props.row)', factory=ToggleHidden)) diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py index f4f99058..debd8fcb 100644 --- a/tailbone/views/employees.py +++ b/tailbone/views/employees.py @@ -167,8 +167,7 @@ class EmployeeView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) def default_view_url(self): if (self.request.has_perm('people.view_profile') diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 097cb229..8f65fc88 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -392,9 +392,8 @@ class MasterView(View): if columns is None: columns = self.get_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_grid(grid) grid.load_settings() return grid @@ -454,10 +453,26 @@ class MasterView(View): if self.sortable or self.pageable or self.filterable: defaults['expose_direct_link'] = True - if 'main_actions' not in kwargs and 'more_actions' not in kwargs: - main, more = self.get_grid_actions() - defaults['main_actions'] = main - defaults['more_actions'] = more + if 'actions' not in kwargs: + + if 'main_actions' in kwargs: + warnings.warn("main_actions param is deprecated for make_grid_kwargs(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + main = kwargs.pop('main_actions') + else: + main = self.get_main_actions() + + if 'more_actions' in kwargs: + warnings.warn("more_actions param is deprecated for make_grid_kwargs(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + more = kwargs.pop('more_actions') + else: + more = self.get_more_actions() + + defaults['actions'] = main + more + defaults.update(kwargs) return defaults @@ -548,9 +563,8 @@ class MasterView(View): if columns is None: columns = self.get_row_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_row_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_row_grid(grid) grid.load_settings() return grid @@ -577,7 +591,7 @@ class MasterView(View): if self.rows_default_pagesize: defaults['default_pagesize'] = self.rows_default_pagesize - if self.has_rows and 'main_actions' not in defaults: + if self.has_rows and 'actions' not in defaults: actions = [] # view action @@ -595,7 +609,7 @@ class MasterView(View): actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url)) defaults['delete_speedbump'] = self.rows_deletable_speedbump - defaults['main_actions'] = actions + defaults['actions'] = actions defaults.update(kwargs) return defaults @@ -630,9 +644,8 @@ class MasterView(View): if columns is None: columns = self.get_version_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_version_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_version_grid(grid) grid.load_settings() return grid @@ -661,9 +674,9 @@ class MasterView(View): 'pageable': True, 'url': lambda txn: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id), } - if 'main_actions' not in kwargs: + if 'actions' not in kwargs: url = lambda txn, i: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id) - defaults['main_actions'] = [ + defaults['actions'] = [ self.make_action('view', icon='eye', url=url), ] defaults.update(kwargs) @@ -1372,7 +1385,7 @@ class MasterView(View): 'sortable': True, 'default_sortkey': 'changed', 'default_sortdir': 'desc', - 'main_actions': [ + 'actions': [ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), self.make_action('view_separate', url=row_url, target='_blank', @@ -3111,6 +3124,11 @@ class MasterView(View): return key def get_grid_actions(self): + """ """ + warnings.warn("get_grid_actions() method is deprecated; " + "please use get_main_actions() or get_more_actions() instead", + DeprecationWarning, stacklevel=2) + main, more = self.get_main_actions(), self.get_more_actions() if len(more) == 1: main, more = main + more, [] diff --git a/tailbone/views/members.py b/tailbone/views/members.py index de844eb7..46ed7e4b 100644 --- a/tailbone/views/members.py +++ b/tailbone/views/members.py @@ -229,8 +229,7 @@ class MemberView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) # equity_total # TODO: should make this configurable diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 163a9a52..020babc5 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -175,8 +175,7 @@ class PersonView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) g.set_link('display_name') g.set_link('first_name') @@ -522,9 +521,9 @@ class PersonView(MasterView): data = self.profile_transactions_query(person) factory = self.get_grid_factory() g = factory( - f'{route_prefix}.profile.transactions.{person.uuid}', - data, - request=self.request, + self.request, + key=f'{route_prefix}.profile.transactions.{person.uuid}', + data=data, model_class=model.Transaction, ajax_data_url=self.get_action_url('view_profile_transactions', person), columns=[ @@ -552,7 +551,7 @@ class PersonView(MasterView): if self.request.has_perm('trainwreck.transactions.view'): url = lambda row, i: self.request.route_url('trainwreck.transactions.view', uuid=row.uuid) - g.main_actions.append(self.make_action('view', icon='eye', url=url)) + g.actions.append(self.make_action('view', icon='eye', url=url)) g.load_settings() g.set_enum('system', self.enum.TRAINWRECK_SYSTEM) @@ -1413,9 +1412,9 @@ class PersonView(MasterView): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() g = factory( - '{}.profile.revisions'.format(route_prefix), - [], # start with empty data! - request=self.request, + self.request, + key=f'{route_prefix}.profile.revisions', + data=[], # start with empty data! columns=[ 'changed', 'changed_by', @@ -1430,7 +1429,7 @@ class PersonView(MasterView): 'changed_by', 'comment', ], - main_actions=[ + actions=[ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), ], diff --git a/tailbone/views/poser/reports.py b/tailbone/views/poser/reports.py index 462df51d..ded80b18 100644 --- a/tailbone/views/poser/reports.py +++ b/tailbone/views/poser/reports.py @@ -110,7 +110,7 @@ class PoserReportView(PoserMasterView): g.set_searchable('description') if self.request.has_perm('report_output.create'): - g.more_actions.append(self.make_action( + g.actions.append(self.make_action( 'generate', icon='arrow-circle-right', url=self.get_generate_url)) diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py index bb799efc..3986f8b0 100644 --- a/tailbone/views/principal.py +++ b/tailbone/views/principal.py @@ -124,11 +124,11 @@ class PrincipalMasterView(MasterView): def find_by_perm_make_results_grid(self, principals): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() - g = factory(key=f'{route_prefix}.results', - request=self.request, + g = factory(self.request, + key=f'{route_prefix}.results', data=[], columns=[], - main_actions=[ + actions=[ self.make_action('view', icon='eye', click_handler='navigateTo(props.row._url)'), ]) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index bf2d7f14..c546a0f4 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -384,7 +384,7 @@ class ProductView(MasterView): g.set_filter('report_code_name', model.ReportCode.name) if self.expose_label_printing and self.has_perm('print_labels'): - g.more_actions.append(self.make_action( + g.actions.append(self.make_action( 'print_label', icon='print', url='#', click_handler='quickLabelPrint(props.row)')) @@ -1197,8 +1197,9 @@ class ProductView(MasterView): # regular price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.regular_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.regular_price_history', + data=data, columns=[ 'price', 'since', @@ -1211,8 +1212,9 @@ class ProductView(MasterView): # current price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.current_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.current_price_history', + data=data, columns=[ 'price', 'price_type', @@ -1229,8 +1231,9 @@ class ProductView(MasterView): # suggested price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.suggested_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.suggested_price_history', + data=data, columns=[ 'price', 'since', @@ -1243,8 +1246,9 @@ class ProductView(MasterView): # cost history data = [] # defer fetching until user asks for it - grid = grids.Grid('products.cost_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.cost_history', + data=data, columns=[ 'cost', 'vendor', @@ -1335,7 +1339,8 @@ class ProductView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.vendor_sources'.format(route_prefix), + self.request, + key=f'{route_prefix}.vendor_sources', data=[], columns=columns, labels={ @@ -1376,7 +1381,8 @@ class ProductView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.lookup_codes'.format(route_prefix), + self.request, + key=f'{route_prefix}.lookup_codes', data=[], columns=[ 'sequence', diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 1d11130c..590b9af5 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -793,8 +793,8 @@ class PurchasingBatchView(BatchMasterView): factory = self.get_grid_factory() g = factory( - key='{}.row_credits'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.row_credits', data=[], columns=[ 'credit_type', diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 0a305f0a..de19a2b9 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -774,8 +774,10 @@ class ReceivingBatchView(PurchasingBatchView): breakdown = self.make_po_vs_invoice_breakdown(batch) factory = self.get_grid_factory() - g = factory('batch_po_vs_invoice_breakdown', [], - columns=['title', 'count']) + g = factory(self.request, + key='batch_po_vs_invoice_breakdown', + data=[], + columns=['title', 'count']) g.set_click_handler('title', "autoFilterPoVsInvoice(props.row)") kwargs['po_vs_invoice_breakdown_data'] = breakdown kwargs['po_vs_invoice_breakdown_grid'] = HTML.literal( @@ -1035,10 +1037,12 @@ class ReceivingBatchView(PurchasingBatchView): icon='shuffle', label="Transform to Unit", url=self.transform_unit_url) - g.more_actions.append(transform) - if g.main_actions and g.main_actions[-1].key == 'delete': - delete = g.main_actions.pop() - g.more_actions.append(delete) + if g.actions and g.actions[-1].key == 'delete': + delete = g.actions.pop() + g.actions.append(transform) + g.actions.append(delete) + else: + g.actions.append(transform) # truck_dump_status if not batch.is_truck_dump_parent(): @@ -1111,7 +1115,7 @@ class ReceivingBatchView(PurchasingBatchView): and self.row_editable(row)): # add the Un-Declare action - g.main_actions.append(self.make_action( + g.actions.append(self.make_action( 'remove', label="Un-Declare", url='#', icon='trash', link_class='has-text-danger', diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index aedda61c..099224be 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -308,7 +308,8 @@ class ReportOutputView(ExportMasterView): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() g = factory( - key='{}.params'.format(route_prefix), + self.request, + key=f'{route_prefix}.params', data=params, columns=['key', 'value'], labels={'key': "Name"}, @@ -705,9 +706,12 @@ class ProblemReportView(MasterView): return ', '.join(recips) def render_days(self, report_info, field): - g = self.get_grid_factory()('days', [], - columns=['weekday_name', 'enabled'], - labels={'weekday_name': "Weekday"}) + factory = self.get_grid_factory() + g = factory(self.request, + key='days', + data=[], + columns=['weekday_name', 'enabled'], + labels={'weekday_name': "Weekday"}) return HTML.literal(g.render_table_element(data_prop='weekdaysData')) def template_kwargs_view(self, **kwargs): diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index fb834479..e8a6d8a2 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -255,8 +255,8 @@ class RoleView(PrincipalMasterView): permission_prefix = self.get_permission_prefix() factory = self.get_grid_factory() g = factory( - key='{}.users'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.users', data=[], columns=[ 'full_name', @@ -269,9 +269,9 @@ class RoleView(PrincipalMasterView): ) if self.request.has_perm('users.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('users.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) return HTML.literal( g.render_table_element(data_prop='usersData')) @@ -366,10 +366,11 @@ class RoleView(PrincipalMasterView): self.make_action('view', icon='zoomin', url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)) ] - kwargs['users'] = grids.Grid(None, users, ['username', 'active'], - request=self.request, + kwargs['users'] = grids.Grid(self.request, + data=users, + columns=['username', 'active'], model_class=model.User, - main_actions=actions) + actions=actions) else: kwargs['users'] = None diff --git a/tailbone/views/tempmon/core.py b/tailbone/views/tempmon/core.py index d551d6e6..7540abbe 100644 --- a/tailbone/views/tempmon/core.py +++ b/tailbone/views/tempmon/core.py @@ -77,8 +77,8 @@ class MasterView(views.MasterView): factory = self.get_grid_factory() g = factory( - key='{}.probes'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.probes', data=[], columns=[ 'description', @@ -96,7 +96,7 @@ class MasterView(views.MasterView): 'critical_temp_max': "Crit. Max", }, linked_columns=['description'], - main_actions=actions, + actions=actions, ) return HTML.literal( g.render_table_element(data_prop='probesData')) diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 9c150c6a..d5f077aa 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -246,10 +246,10 @@ class TransactionView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.custorder_xref_markers'.format(route_prefix), + self.request, + key=f'{route_prefix}.custorder_xref_markers', data=[], - columns=['custorder_xref', 'custorder_item_xref'], - request=self.request) + columns=['custorder_xref', 'custorder_item_xref']) return HTML.literal( g.render_table_element(data_prop='custorderXrefMarkersData')) @@ -355,11 +355,11 @@ class TransactionView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.discounts'.format(route_prefix), + self.request, + key=f'{route_prefix}.discounts', data=[], columns=['discount_type', 'description', 'amount'], - labels={'discount_type': "Type"}, - request=self.request) + labels={'discount_type': "Type"}) return HTML.literal( g.render_table_element(data_prop='discountsData')) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 9eae74d8..9b533efe 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -44,9 +44,6 @@ class UserView(PrincipalMasterView): Master view for the User model. """ model_class = User - has_rows = True - rows_title = "User Events" - model_row_class = UserEvent has_versions = True touchable = True mergeable = True @@ -77,6 +74,11 @@ class UserView(PrincipalMasterView): 'permissions', ] + has_rows = True + model_row_class = UserEvent + rows_title = "User Events" + rows_viewable = False + row_grid_columns = [ 'type_code', 'occurred', @@ -297,11 +299,11 @@ class UserView(PrincipalMasterView): factory = self.get_grid_factory() g = factory( - request=self.request, - key='{}.api_tokens'.format(route_prefix), + self.request, + key=f'{route_prefix}.api_tokens', data=[], columns=['description', 'created'], - main_actions=[ + actions=[ self.make_action('delete', icon='trash', click_handler="$emit('api-token-delete', props.row)")]) @@ -514,7 +516,6 @@ class UserView(PrincipalMasterView): g.set_sort_defaults('occurred', 'desc') g.set_enum('type_code', self.enum.USER_EVENT) g.set_label('type_code', "Event Type") - g.main_actions = [] def get_version_child_classes(self): model = self.model diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 0a8d5d66..0d0fe112 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -12,9 +12,8 @@ class TestGrid(WebTestCase): self.setup_web() self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler') - def make_grid(self, key, data=[], **kwargs): - kwargs.setdefault('request', self.request) - return mod.Grid(key, data=data, **kwargs) + def make_grid(self, key=None, data=[], **kwargs): + return mod.Grid(self.request, key=key, data=data, **kwargs) def test_basic(self): grid = self.make_grid('foo') @@ -90,6 +89,50 @@ class TestGrid(WebTestCase): grid = self.make_grid('foo', main_actions=['foo'], more_actions=['bar']) self.assertEqual(grid.actions, ['foo', 'bar']) + def test_set_label(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + self.assertEqual(grid.labels, {}) + + # basic + grid.set_label('name', "NAME COL") + self.assertEqual(grid.labels['name'], "NAME COL") + + # can replace label + grid.set_label('name', "Different") + self.assertEqual(grid.labels['name'], "Different") + self.assertEqual(grid.get_label('name'), "Different") + + # can update only column, not filter + self.assertEqual(grid.labels, {'name': "Different"}) + self.assertIn('name', grid.filters) + self.assertEqual(grid.filters['name'].label, "Different") + grid.set_label('name', "COLUMN ONLY", column_only=True) + self.assertEqual(grid.get_label('name'), "COLUMN ONLY") + self.assertEqual(grid.filters['name'].label, "Different") + + def test_get_view_click_handler(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + + grid.actions.append( + mod.GridAction(self.request, 'view', + click_handler='clickHandler(props.row)')) + + handler = grid.get_view_click_handler() + self.assertEqual(handler, 'clickHandler(props.row)') + + def test_set_action_urls(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + + grid.actions.append( + mod.GridAction(self.request, 'view', url='/blarg')) + + setting = {'name': 'foo', 'value': 'bar'} + grid.set_action_urls(setting, setting, 0) + self.assertEqual(setting['_action_url_view'], '/blarg') + def test_render_vue_tag(self): model = self.app.model From 9da2a148c65ebde63b39903028bdf77577d53780 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 18:45:04 -0500 Subject: [PATCH 005/102] feat: move "basic" grid pagination logic to wuttaweb so far only "simple" pagination is supported by wuttaweb, so basically the main feature flag, page size, current page. in this scenario *all* data is written to client-side JSON and Buefy handles the actual pagination. backend pagination coming soon for wuttaweb but for now tailbone still handles all that. --- tailbone/grids/core.py | 130 +++++++++++++++++-------- tailbone/templates/grids/complete.mako | 18 ++-- tailbone/views/master.py | 4 +- tailbone/views/wutta/people.py | 4 + tests/grids/test_core.py | 86 ++++++++++++++++ 5 files changed, 195 insertions(+), 47 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index a5617215..0b23fb78 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -31,6 +31,7 @@ import logging import sqlalchemy as sa from sqlalchemy import orm +from wuttjamaican.util import UNSPECIFIED from rattail.db.types import GPCType from rattail.util import prettify, pretty_boolean @@ -209,9 +210,6 @@ class Grid(WuttaGrid): sorters={}, default_sortkey=None, default_sortdir='asc', - pageable=False, - default_pagesize=None, - default_page=1, checkboxes=False, checked=None, check_handler=None, @@ -233,7 +231,26 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) - # TODO: pretty sure this should go away? + if kwargs.get('pageable'): + warnings.warn("component param is deprecated for Grid(); " + "please use vue_tagname param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('paginated', kwargs.pop('pageable')) + + if kwargs.get('default_pagesize'): + warnings.warn("default_pagesize param is deprecated for Grid(); " + "please use pagesize param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('pagesize', kwargs.pop('default_pagesize')) + + if kwargs.get('default_page'): + warnings.warn("default_page param is deprecated for Grid(); " + "please use page param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('page', kwargs.pop('default_page')) + + # TODO: this should not be needed once all templates correctly + # reference grid.vue_component etc. kwargs.setdefault('vue_tagname', 'tailbone-grid') kwargs['key'] = key @@ -272,10 +289,6 @@ class Grid(WuttaGrid): self.default_sortkey = default_sortkey self.default_sortdir = default_sortdir - self.pageable = pageable - self.default_pagesize = default_pagesize - self.default_page = default_page - self.checkboxes = checkboxes self.checked = checked if self.checked is None: @@ -333,6 +346,16 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) return self.vue_component + def get_pageable(self): + """ """ + return self.paginated + + def set_pageable(self, value): + """ """ + self.paginated = value + + pageable = property(get_pageable, set_pageable) + def hide_column(self, key): """ This *removes* a column from the grid, altogether. @@ -756,18 +779,61 @@ class Grid(WuttaGrid): keyfunc = lambda v: v[key] return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc') - def get_default_pagesize(self): + def get_pagesize_options(self, default=None): + """ """ + # let upstream check config + options = super().get_pagesize_options(default=UNSPECIFIED) + if options is not UNSPECIFIED: + return options + + # fallback to legacy config + options = self.config.get_list('tailbone.grid.pagesize_options') + if options: + warnings.warn("tailbone.grid.pagesize_options setting is deprecated; " + "please set wuttaweb.grids.default_pagesize_options instead", + DeprecationWarning) + options = [int(size) for size in options + if size.isdigit()] + if options: + return options + + if default: + return default + + # use upstream default + return super().get_pagesize_options() + + def get_pagesize(self, default=None): + """ """ + # let upstream check config + pagesize = super().get_pagesize(default=UNSPECIFIED) + if pagesize is not UNSPECIFIED: + return pagesize + + # fallback to legacy config + pagesize = self.config.get_int('tailbone.grid.default_pagesize') + if pagesize: + warnings.warn("tailbone.grid.default_pagesize setting is deprecated; " + "please use wuttaweb.grids.default_pagesize instead", + DeprecationWarning) + return pagesize + + if default: + return default + + # use upstream default + return super().get_pagesize() + + def get_default_pagesize(self): # pragma: no cover + """ """ + warnings.warn("Grid.get_default_pagesize() method is deprecated; " + "please use Grid.get_pagesize() of Grid.page instead", + DeprecationWarning, stacklevel=2) + if self.default_pagesize: return self.default_pagesize - pagesize = self.request.rattail_config.getint('tailbone', - 'grid.default_pagesize', - default=0) - if pagesize: - return pagesize - - options = self.get_pagesize_options() - return options[0] + return self.get_pagesize() def load_settings(self, store=True): """ @@ -789,9 +855,9 @@ class Grid(WuttaGrid): settings['sorters.1.dir'] = self.default_sortdir else: settings['sorters.length'] = 0 - if self.pageable: - settings['pagesize'] = self.get_default_pagesize() - settings['page'] = self.default_page + if self.paginated: + settings['pagesize'] = self.pagesize + settings['page'] = self.page if self.filterable: for filtr in self.iter_filters(): settings['filter.{}.active'.format(filtr.key)] = filtr.default_active @@ -867,7 +933,7 @@ class Grid(WuttaGrid): 'field': settings[f'sorters.{i}.key'], 'order': settings[f'sorters.{i}.dir'], }) - if self.pageable: + if self.paginated: self.pagesize = settings['pagesize'] self.page = settings['page'] @@ -971,7 +1037,7 @@ class Grid(WuttaGrid): merge(f'sorters.{i}.key') merge(f'sorters.{i}.dir') - if self.pageable: + if self.paginated: merge('pagesize', int) merge('page', int) @@ -1154,7 +1220,7 @@ class Grid(WuttaGrid): :param settings: Dictionary of initial settings, which is to be updated. """ - if not self.pageable: + if not self.paginated: return pagesize = self.request.GET.get('pagesize') @@ -1231,7 +1297,7 @@ class Grid(WuttaGrid): persist(f'sorters.{i}.key') persist(f'sorters.{i}.dir') - if self.pageable: + if self.paginated: persist('pagesize') persist('page') @@ -1355,7 +1421,7 @@ class Grid(WuttaGrid): data = self.filter_data(data) if self.sortable: data = self.sort_data(data) - if self.pageable: + if self.paginated: self.pager = self.paginate_data(data) data = self.pager return data @@ -1580,18 +1646,6 @@ class Grid(WuttaGrid): return tags.checkbox('checkbox-{}-{}'.format(self.key, self.get_row_key(item)), checked=self.checked(item)) - def get_pagesize_options(self): - - # use values from config, if defined - options = self.request.rattail_config.getlist('tailbone', 'grid.pagesize_options') - if options: - options = [int(size) for size in options - if size.isdigit()] - if options: - return options - - return [5, 10, 20, 50, 100, 200] - def has_static_data(self): """ Should return ``True`` if the grid data can be considered "static" @@ -1734,7 +1788,7 @@ class Grid(WuttaGrid): results['checked_rows_code'] = '[{}]'.format( ', '.join(['{}[{}]'.format(var, i) for i in checked])) - if self.pageable and self.pager is not None: + if self.paginated and self.pager is not None: results['total_items'] = self.pager.item_count results['per_page'] = self.pager.items_per_page results['page'] = self.pager.page diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 93bb6c26..53043803 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -107,12 +107,14 @@ @cellclick="cellClick" % endif + % if grid.paginated: :paginated="paginated" :per-page="perPage" :current-page="currentPage" backend-pagination :total="total" @page-change="onPageChange" + % endif ## TODO: should let grid (or master view) decide how to set these? icon-pack="fas" @@ -203,7 +205,7 @@
% endif - % if getattr(grid, 'pageable', False): + % if grid.paginated:
@@ -255,12 +257,14 @@ checkedRows: ${grid_data['checked_rows_code']|n}, % endif - paginated: ${json.dumps(getattr(grid, 'pageable', False))|n}, + % if grid.paginated: + paginated: ${json.dumps(grid.paginated)|n}, total: ${len(grid_data['data']) if static_data else (grid_data['total_items'] if grid_data is not Undefined else 0)}, - perPage: ${json.dumps(grid.pagesize if getattr(grid, 'pageable', False) else None)|n}, - currentPage: ${json.dumps(grid.page if getattr(grid, 'pageable', False) else None)|n}, - firstItem: ${json.dumps(grid_data['first_item'] if getattr(grid, 'pageable', False) else None)|n}, - lastItem: ${json.dumps(grid_data['last_item'] if getattr(grid, 'pageable', False) else None)|n}, + perPage: ${json.dumps(grid.pagesize if grid.paginated else None)|n}, + currentPage: ${json.dumps(grid.page if grid.paginated else None)|n}, + firstItem: ${json.dumps(grid_data['first_item'] if grid.paginated else None)|n}, + lastItem: ${json.dumps(grid_data['last_item'] if grid.paginated else None)|n}, + % endif % if getattr(grid, 'sortable', False): @@ -439,7 +443,7 @@ params['sort'+i+'dir'] = this.backendSorters[i-1].order } % endif - % if getattr(grid, 'pageable', False): + % if grid.paginated: params.pagesize = this.perPage params.page = this.currentPage % endif diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 8f65fc88..58b93568 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -439,7 +439,7 @@ class MasterView(View): 'filterable': self.filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.sortable, - 'pageable': self.pageable, + 'paginated': self.pageable, 'extra_row_class': self.grid_extra_class, 'url': lambda obj: self.get_action_url('view', obj), 'checkboxes': checkboxes, @@ -589,7 +589,7 @@ class MasterView(View): } if self.rows_default_pagesize: - defaults['default_pagesize'] = self.rows_default_pagesize + defaults['pagesize'] = self.rows_default_pagesize if self.has_rows and 'actions' not in defaults: actions = [] diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index c92e34ae..3158b478 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -45,6 +45,10 @@ class PersonView(wutta.PersonView): model_class = Person Session = Session + # TODO: /grids/complete.mako is too aggressive for the + # limited support we have in wuttaweb thus far + paginated = False + labels = { 'display_name': "Full Name", } diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 0d0fe112..7cba917a 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -19,6 +19,32 @@ class TestGrid(WebTestCase): grid = self.make_grid('foo') self.assertIsInstance(grid, mod.Grid) + def test_deprecated_params(self): + + # component + grid = self.make_grid() + self.assertEqual(grid.vue_tagname, 'tailbone-grid') + grid = self.make_grid(component='blarg') + self.assertEqual(grid.vue_tagname, 'blarg') + + # pageable + grid = self.make_grid() + self.assertFalse(grid.paginated) + grid = self.make_grid(pageable=True) + self.assertTrue(grid.paginated) + + # default_pagesize + grid = self.make_grid() + self.assertEqual(grid.pagesize, 20) + grid = self.make_grid(default_pagesize=15) + self.assertEqual(grid.pagesize, 15) + + # default_page + grid = self.make_grid() + self.assertEqual(grid.page, 1) + grid = self.make_grid(default_page=42) + self.assertEqual(grid.page, 42) + def test_vue_tagname(self): # default @@ -133,6 +159,66 @@ class TestGrid(WebTestCase): grid.set_action_urls(setting, setting, 0) self.assertEqual(setting['_action_url_view'], '/blarg') + def test_pageable(self): + grid = self.make_grid() + self.assertFalse(grid.paginated) + grid.pageable = True + self.assertTrue(grid.paginated) + grid.paginated = False + self.assertFalse(grid.pageable) + + def test_get_pagesize_options(self): + grid = self.make_grid() + + # default + options = grid.get_pagesize_options() + self.assertEqual(options, [5, 10, 20, 50, 100, 200]) + + # override default + options = grid.get_pagesize_options(default=[42]) + self.assertEqual(options, [42]) + + # from legacy config + self.config.setdefault('tailbone.grid.pagesize_options', '1 2 3') + grid = self.make_grid() + options = grid.get_pagesize_options() + self.assertEqual(options, [1, 2, 3]) + + # from new config + self.config.setdefault('wuttaweb.grids.default_pagesize_options', '4, 5, 6') + grid = self.make_grid() + options = grid.get_pagesize_options() + self.assertEqual(options, [4, 5, 6]) + + def test_get_pagesize(self): + grid = self.make_grid() + + # default + size = grid.get_pagesize() + self.assertEqual(size, 20) + + # override default + size = grid.get_pagesize(default=42) + self.assertEqual(size, 42) + + # override default options + self.config.setdefault('wuttaweb.grids.default_pagesize_options', '10 15 30') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 10) + + # from legacy config + self.config.setdefault('tailbone.grid.default_pagesize', '12') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 12) + + # from new config + self.config.setdefault('wuttaweb.grids.default_pagesize', '15') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 15) + def test_render_vue_tag(self): model = self.app.model From f4c8176d8325f052e4aa46666b6ae9d5a5779e75 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 22:54:22 -0500 Subject: [PATCH 006/102] =?UTF-8?q?bump:=20version=200.17.0=20=E2=86=92=20?= =?UTF-8?q?0.18.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5724e685..0671e03b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.18.0 (2024-08-16) + +### Feat + +- move "basic" grid pagination logic to wuttaweb +- inherit from wutta base class for Grid +- inherit most logic from wuttaweb, for GridAction + +### Fix + +- avoid route error in user view, when using wutta people view +- fix some more wutta compat for base template + ## v0.17.0 (2024-08-15) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 31c7ef8d..bd4882c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.17.0" +version = "0.18.0" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.8.1", + "WuttaWeb>=0.9.0", "zope.sqlalchemy>=1.5", ] From 5e82fe3946d4a65c67527b704198ac5a8d73c6e1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 10:20:09 -0500 Subject: [PATCH 007/102] fix: fix broken permission directives in web api startup --- tailbone/webapi.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tailbone/webapi.py b/tailbone/webapi.py index 7c0e9b41..d0edb412 100644 --- a/tailbone/webapi.py +++ b/tailbone/webapi.py @@ -85,8 +85,15 @@ def make_pyramid_config(settings): provider.configure_db_sessions(rattail_config, pyramid_config) # add some permissions magic - pyramid_config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group') - pyramid_config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission') + pyramid_config.add_directive('add_wutta_permission_group', + 'wuttaweb.auth.add_permission_group') + pyramid_config.add_directive('add_wutta_permission', + 'wuttaweb.auth.add_permission') + # TODO: deprecate / remove these + pyramid_config.add_directive('add_tailbone_permission_group', + 'wuttaweb.auth.add_permission_group') + pyramid_config.add_directive('add_tailbone_permission', + 'wuttaweb.auth.add_permission') return pyramid_config From c95e42bf828b93f22247660e15df67f2c431a5c4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 17 Aug 2024 11:05:15 -0500 Subject: [PATCH 008/102] fix: fix misc. errors in grid template per wuttaweb --- tailbone/templates/grids/complete.mako | 91 +++++++++++++++++++------- tailbone/views/master.py | 5 +- tailbone/views/wutta/people.py | 10 --- 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 53043803..d3981a16 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -107,13 +107,17 @@ @cellclick="cellClick" % endif + ## paging % if grid.paginated: - :paginated="paginated" - :per-page="perPage" - :current-page="currentPage" - backend-pagination - :total="total" - @page-change="onPageChange" + paginated + pagination-size="is-small" + :per-page="perPage" + :current-page="currentPage" + @page-change="onPageChange" + % if grid.paginate_on_backend: + backend-pagination + :total="pagerStats.item_count" + % endif % endif ## TODO: should let grid (or master view) decide how to set these? @@ -206,12 +210,13 @@ % endif % if grid.paginated: -
showing - {{ firstItem.toLocaleString('en') }} - {{ lastItem.toLocaleString('en') }} - of {{ total.toLocaleString('en') }} results; + {{ renderNumber(pagerStats.first_item) }} + - {{ renderNumber(pagerStats.last_item) }} + of {{ renderNumber(pagerStats.item_count) }} results; data.length) { + last = data.length + } + return { + 'item_count': data.length, + 'items_per_page': this.perPage, + 'page': this.currentPage, + 'first_item': first, + 'last_item': last, + } + }, + + % endif + addFilterChoices() { // nb. this returns all choices available for "Add Filter" operation @@ -373,6 +405,12 @@ methods: { + renderNumber(value) { + if (value != undefined) { + return value.toLocaleString('en') + } + }, + formatAddFilterItem(filtr) { if (!filtr.key) { filtr = this.filters[filtr] @@ -486,23 +524,23 @@ params = params.toString() this.loading = true - this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => { - if (!data.error) { - ${grid.vue_component}CurrentData = data.data + this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => { + if (!response.data.error) { + ${grid.vue_component}CurrentData = response.data.data.data this.data = ${grid.vue_component}CurrentData - this.rowStatusMap = data.row_status_map - this.total = data.total_items - this.firstItem = data.first_item - this.lastItem = data.last_item + % if grid.paginated and grid.paginate_on_backend: + this.pagerStats = response.data.pager_stats + % endif + this.rowStatusMap = response.data.data.row_status_map this.loading = false this.savingDefaults = false - this.checkedRows = this.locateCheckedRows(data.checked_rows) + this.checkedRows = this.locateCheckedRows(response.data.data.checked_rows) if (success) { success() } } else { this.$buefy.toast.open({ - message: data.error, + message: response.data.error, type: 'is-danger', duration: 2000, // 4 seconds }) @@ -514,8 +552,11 @@ } }) .catch((error) => { + ${grid.vue_component}CurrentData = [] this.data = [] - this.total = 0 + % if grid.paginated and grid.paginate_on_backend: + this.pagerStats = {} + % endif this.loading = false this.savingDefaults = false if (failure) { diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 58b93568..1fa0ae40 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -346,7 +346,10 @@ class MasterView(View): # return grid data only, if partial page was requested if self.request.params.get('partial'): - return self.json_response(grid.get_table_data()) + context = {'data': grid.get_table_data()} + if grid.paginated and grid.paginate_on_backend: + context['pager_stats'] = grid.get_vue_pager_stats() + return self.json_response(context) context = { 'grid': grid, diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index 3158b478..c10020ea 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -45,10 +45,6 @@ class PersonView(wutta.PersonView): model_class = Person Session = Session - # TODO: /grids/complete.mako is too aggressive for the - # limited support we have in wuttaweb thus far - paginated = False - labels = { 'display_name': "Full Name", } @@ -91,12 +87,6 @@ class PersonView(wutta.PersonView): # display_name g.set_link('display_name') - # first_name - g.set_link('first_name') - - # last_name - g.set_link('last_name') - # merge_requested g.set_label('merge_requested', "MR") g.set_renderer('merge_requested', self.render_merge_requested) From ec36df4a341a1e8c7ba5821fa270fdb1125b1848 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 14:05:52 -0500 Subject: [PATCH 009/102] feat: move single-column grid sorting logic to wuttaweb --- tailbone/forms/core.py | 26 +-- tailbone/grids/core.py | 272 ++++++++++++++----------- tailbone/templates/grids/complete.mako | 63 +++--- tailbone/views/master.py | 28 +-- tailbone/views/wutta/people.py | 8 +- tests/grids/test_core.py | 245 +++++++++++++++++++++- tests/views/test_master.py | 33 ++- 7 files changed, 475 insertions(+), 200 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index eeae4537..704d3b54 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore from pyramid.renderers import render from webhelpers2.html import tags, HTML -from wuttaweb.util import get_form_data, make_json_safe +from wuttaweb.util import FieldList, get_form_data, make_json_safe from tailbone.db import Session from tailbone.util import raw_datetime, render_markdown @@ -1418,30 +1418,6 @@ class Form(object): return False -class FieldList(list): - """ - Convenience wrapper for a form's field list. - """ - - def insert_before(self, field, newfield): - if field in self: - i = self.index(field) - self.insert(i, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - def insert_after(self, field, newfield): - if field in self: - i = self.index(field) - self.insert(i + 1, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - @colander.deferred def upload_widget(node, kw): request = kw['request'] diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 0b23fb78..cc1888fb 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -39,7 +39,8 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage -from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction +from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction, SortInfo +from wuttaweb.util import FieldList from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -48,23 +49,17 @@ from tailbone.util import raw_datetime log = logging.getLogger(__name__) -class FieldList(list): - """ - Convenience wrapper for a field list. - """ - - def insert_before(self, field, newfield): - i = self.index(field) - self.insert(i, newfield) - - def insert_after(self, field, newfield): - i = self.index(field) - self.insert(i + 1, newfield) - - class Grid(WuttaGrid): """ - Core grid class. In sore need of documentation. + Base class for all grids. + + This is now a subclass of + :class:`wuttaweb:wuttaweb.grids.base.Grid`, and exists to add + customizations which have traditionally been part of Tailbone. + + Some of these customizations are still undocumented. Some will + eventually be moved to the upstream/parent class, and possibly + some will be removed outright. What docs we have, are shown here. .. _Buefy docs: https://buefy.org/documentation/table/ @@ -206,10 +201,6 @@ class Grid(WuttaGrid): filters={}, use_byte_string_filters=False, searchable={}, - sortable=False, - sorters={}, - default_sortkey=None, - default_sortdir='asc', checkboxes=False, checked=None, check_handler=None, @@ -231,6 +222,20 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) + if kwargs.get('default_sortkey'): + warnings.warn("default_sortkey param is deprecated for Grid(); " + "please use sort_defaults param instead", + DeprecationWarning, stacklevel=2) + if kwargs.get('default_sortdir'): + warnings.warn("default_sortdir param is deprecated for Grid(); " + "please use sort_defaults param instead", + DeprecationWarning, stacklevel=2) + if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'): + sortkey = kwargs.pop('default_sortkey', None) + sortdir = kwargs.pop('default_sortdir', 'asc') + if sortkey: + kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) + if kwargs.get('pageable'): warnings.warn("component param is deprecated for Grid(); " "please use vue_tagname param instead", @@ -284,11 +289,6 @@ class Grid(WuttaGrid): self.searchable = searchable or {} - self.sortable = sortable - self.sorters = self.make_sorters(sorters) - self.default_sortkey = default_sortkey - self.default_sortdir = default_sortdir - self.checkboxes = checkboxes self.checked = checked if self.checked is None: @@ -328,9 +328,7 @@ class Grid(WuttaGrid): @property def component(self): - """ - DEPRECATED - use :attr:`vue_tagname` instead. - """ + """ """ warnings.warn("Grid.component is deprecated; " "please use vue_tagname instead", DeprecationWarning, stacklevel=2) @@ -338,20 +336,66 @@ class Grid(WuttaGrid): @property def component_studly(self): - """ - DEPRECATED - use :attr:`vue_component` instead. - """ + """ """ warnings.warn("Grid.component_studly is deprecated; " "please use vue_component instead", DeprecationWarning, stacklevel=2) return self.vue_component + def get_default_sortkey(self): + """ """ + warnings.warn("Grid.default_sortkey is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + return self.sort_defaults[0].sortkey + + def set_default_sortkey(self, value): + """ """ + warnings.warn("Grid.default_sortkey is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + info = self.sort_defaults[0] + self.sort_defaults[0] = SortInfo(value, info.sortdir) + else: + self.sort_defaults = [SortInfo(value, 'asc')] + + default_sortkey = property(get_default_sortkey, set_default_sortkey) + + def get_default_sortdir(self): + """ """ + warnings.warn("Grid.default_sortdir is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + return self.sort_defaults[0].sortdir + + def set_default_sortdir(self, value): + """ """ + warnings.warn("Grid.default_sortdir is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + info = self.sort_defaults[0] + self.sort_defaults[0] = SortInfo(info.sortkey, value) + else: + raise ValueError("cannot set default_sortdir without default_sortkey") + + default_sortdir = property(get_default_sortdir, set_default_sortdir) + def get_pageable(self): """ """ + warnings.warn("Grid.pageable is deprecated; " + "please use Grid.paginated instead", + DeprecationWarning, stacklevel=2) return self.paginated def set_pageable(self, value): """ """ + warnings.warn("Grid.pageable is deprecated; " + "please use Grid.paginated instead", + DeprecationWarning, stacklevel=2) self.paginated = value pageable = property(get_pageable, set_pageable) @@ -405,18 +449,30 @@ class Grid(WuttaGrid): self.joiners[key] = joiner def set_sorter(self, key, *args, **kwargs): - if len(args) == 1 and args[0] is None: - self.remove_sorter(key) + """ """ + + if len(args) == 1: + if kwargs: + warnings.warn("kwargs are ignored for Grid.set_sorter(); " + "please refactor your code accordingly", + DeprecationWarning, stacklevel=2) + if args[0] is None: + warnings.warn("specifying None is deprecated for Grid.set_sorter(); " + "please use Grid.remove_sorter() instead", + DeprecationWarning, stacklevel=2) + self.remove_sorter(key) + else: + super().set_sorter(key, args[0]) + + elif len(args) == 0: + super().set_sorter(key) + else: + warnings.warn("multiple args are deprecated for Grid.set_sorter(); " + "please refactor your code accordingly", + DeprecationWarning, stacklevel=2) self.sorters[key] = self.make_sorter(*args, **kwargs) - def remove_sorter(self, key): - self.sorters.pop(key, None) - - def set_sort_defaults(self, sortkey, sortdir='asc'): - self.default_sortkey = sortkey - self.default_sortdir = sortdir - def set_filter(self, key, *args, **kwargs): if len(args) == 1 and args[0] is None: self.remove_filter(key) @@ -731,53 +787,12 @@ class Grid(WuttaGrid): if filtr.active: yield filtr - def make_sorters(self, sorters=None): - """ - Returns an initial set of sorters which will be available to the grid. - The grid itself may or may not provide some default sorters, and the - ``sorters`` kwarg may contain additions and/or overrides. - """ - sorters, updates = {}, sorters - if self.model_class: - mapper = orm.class_mapper(self.model_class) - for prop in mapper.iterate_properties: - if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'): - sorters[prop.key] = self.make_sorter(prop) - if updates: - sorters.update(updates) - return sorters - - def make_sorter(self, model_property): - """ - Returns a function suitable for a sort map callable, with typical logic - built in for sorting applied to ``field``. - """ - class_ = getattr(model_property, 'class_', self.model_class) - column = getattr(class_, model_property.key) - - def sorter(query, direction): - # TODO: this seems hacky..normally we expect a true query - # of course, but in some cases it may be a list instead. - # if so then we can't actually sort - if isinstance(query, list): - return query - return query.order_by(getattr(column, direction)()) - - sorter._class = class_ - sorter._column = column - - return sorter - def make_simple_sorter(self, key, foldcase=False): - """ - Returns a function suitable for a sort map callable, with typical logic - built in for sorting a data set comprised of dicts, on the given key. - """ - if foldcase: - keyfunc = lambda v: v[key].lower() - else: - keyfunc = lambda v: v[key] - return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc') + """ """ + warnings.warn("Grid.make_simple_sorter() is deprecated; " + "please use Grid.make_sorter() instead", + DeprecationWarning, stacklevel=2) + return self.make_sorter(key, foldcase=foldcase) def get_pagesize_options(self, default=None): """ """ @@ -849,10 +864,17 @@ class Grid(WuttaGrid): # initial default settings settings = {} if self.sortable: - if self.default_sortkey: + if self.sort_defaults: + sort_defaults = self.sort_defaults + if len(sort_defaults) > 1: + log.warning("multiple sort defaults are not yet supported; " + "list will be pruned to first element for '%s' grid: %s", + self.key, sort_defaults) + sort_defaults = [sort_defaults[0]] + sortinfo = sort_defaults[0] settings['sorters.length'] = 1 - settings['sorters.1.key'] = self.default_sortkey - settings['sorters.1.dir'] = self.default_sortdir + settings['sorters.1.key'] = sortinfo.sortkey + settings['sorters.1.dir'] = sortinfo.sortdir else: settings['sorters.length'] = 0 if self.paginated: @@ -927,11 +949,12 @@ class Grid(WuttaGrid): filtr.verb = settings['filter.{}.verb'.format(filtr.key)] filtr.value = settings['filter.{}.value'.format(filtr.key)] if self.sortable: + # and self.sort_on_backend: self.active_sorters = [] for i in range(1, settings['sorters.length'] + 1): self.active_sorters.append({ - 'field': settings[f'sorters.{i}.key'], - 'order': settings[f'sorters.{i}.dir'], + 'key': settings[f'sorters.{i}.key'], + 'dir': settings[f'sorters.{i}.dir'], }) if self.paginated: self.pagesize = settings['pagesize'] @@ -1321,21 +1344,24 @@ class Grid(WuttaGrid): return data - def sort_data(self, data): - """ - Sort the given query according to current settings, and return the result. - """ - # bail if no sort settings - if not self.active_sorters: + def sort_data(self, data, sorters=None): + """ """ + if sorters is None: + sorters = self.active_sorters + if not sorters: return data - # TODO: is there a better way to check for SA sorting? - if self.model_class: + # sqlalchemy queries require special handling, in case of + # multi-column sorting + if isinstance(data, orm.Query): # collect actual column sorters for order_by clause - sorters = [] - for sorter in self.active_sorters: - sortkey = sorter['field'] + query_sorters = [] + for sorter in sorters: + sortkey = sorter['key'] + sortdir = sorter['dir'] + + # cannot sort unless we have a sorter callable sortfunc = self.sorters.get(sortkey) if not sortfunc: log.warning("unknown sorter: %s", sorter) @@ -1347,34 +1373,36 @@ class Grid(WuttaGrid): self.joined.add(sortkey) # add column/dir to collection - sortdir = sorter['order'] - sorters.append(getattr(sortfunc._column, sortdir)()) + query_sorters.append(getattr(sortfunc._column, sortdir)()) # apply sorting to query - if sorters: - data = data.order_by(*sorters) + if query_sorters: + data = data.order_by(*query_sorters) return data - else: - # not a SQLAlchemy grid, custom sorter + # manual sorting; only one column allowed + if len(sorters) != 1: + raise NotImplementedError("mulit-column manual sorting not yet supported") - assert len(self.active_sorters) < 2 + # our one and only active sorter + sorter = sorters[0] + sortkey = sorter['key'] + sortdir = sorter['dir'] - sortkey = self.active_sorters[0]['field'] - sortdir = self.active_sorters[0]['order'] or 'asc' + # cannot sort unless we have a sorter callable + sortfunc = self.sorters.get(sortkey) + if not sortfunc: + return data - # Cannot sort unless we have a sort function. - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data + # apply joins needed for this sorter + # TODO: is this actually relevant for manual sort? + if sortkey in self.joiners and sortkey not in self.joined: + data = self.joiners[sortkey](data) + self.joined.add(sortkey) - # apply joins needed for this sorter - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) - - return sortfunc(data, sortdir) + # invoke the sorter + return sortfunc(data, sortdir) def paginate_data(self, data): """ @@ -1671,7 +1699,7 @@ class Grid(WuttaGrid): columns.append({ 'field': name, 'label': self.get_label(name), - 'sortable': self.sortable and name in self.sorters, + 'sortable': self.is_sortable(name), 'visible': name not in self.invisible, }) return columns diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index d3981a16..5a005c2e 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -81,7 +81,11 @@ % endif % endif - % if getattr(grid, 'sortable', False): + ## sorting + % if grid.sortable: + ## nb. buefy only supports *one* default sorter + :default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null" + backend-sorting @sort="onSort" @sorting-priority-removed="sortingPriorityRemoved" @@ -93,8 +97,6 @@ ## https://github.com/buefy/buefy/issues/2584 :sort-multiple="allowMultiSort" - ## nb. specify default sort only if single-column - :default-sort="backendSorters.length == 1 ? [backendSorters[0].field, backendSorters[0].order] : null" ## nb. otherwise there may be default multi-column sort :sort-multiple-data="sortingPriority" @@ -272,7 +274,9 @@ % endif % endif - % if getattr(grid, 'sortable', False): + ## sorting + % if grid.sortable: + sorters: ${json.dumps(grid.active_sorters)|n}, ## TODO: there is a bug (?) which prevents the arrow from ## displaying for simple default single-column sort. so to @@ -281,10 +285,7 @@ ## https://github.com/buefy/buefy/issues/2584 allowMultiSort: false, - ## nb. this contains all truly active sorters - backendSorters: ${json.dumps(grid.active_sorters)|n}, - - ## nb. whereas this will only contain multi-column sorters, + ## nb. this will only contain multi-column sorters, ## but will be *empty* for single-column sorting % if len(grid.active_sorters) > 1: sortingPriority: ${json.dumps(grid.active_sorters)|n}, @@ -474,17 +475,18 @@ }, getBasicParams() { - let params = {} - % if getattr(grid, 'sortable', False): - for (let i = 1; i <= this.backendSorters.length; i++) { - params['sort'+i+'key'] = this.backendSorters[i-1].field - params['sort'+i+'dir'] = this.backendSorters[i-1].order + const params = { + % if grid.paginated and grid.paginate_on_backend: + pagesize: this.perPage, + page: this.currentPage, + % endif + } + % if grid.sortable and grid.sort_on_backend: + for (let i = 1; i <= this.sorters.length; i++) { + params['sort'+i+'key'] = this.sorters[i-1].key + params['sort'+i+'dir'] = this.sorters[i-1].dir } % endif - % if grid.paginated: - params.pagesize = this.perPage - params.page = this.currentPage - % endif return params }, @@ -526,15 +528,15 @@ this.loading = true this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => { if (!response.data.error) { - ${grid.vue_component}CurrentData = response.data.data.data + ${grid.vue_component}CurrentData = response.data.data this.data = ${grid.vue_component}CurrentData % if grid.paginated and grid.paginate_on_backend: this.pagerStats = response.data.pager_stats % endif - this.rowStatusMap = response.data.data.row_status_map + this.rowStatusMap = response.data.row_status_map || {} this.loading = false this.savingDefaults = false - this.checkedRows = this.locateCheckedRows(response.data.data.checked_rows) + this.checkedRows = this.locateCheckedRows(response.data.checked_rows || []) if (success) { success() } @@ -597,26 +599,26 @@ onSort(field, order, event) { - // nb. buefy passes field name, oruga passes object - if (field.field) { + ## nb. buefy passes field name; oruga passes field object + % if request.use_oruga: field = field.field - } + % endif if (event.ctrlKey) { // engage or enhance multi-column sorting - let sorter = this.backendSorters.filter(i => i.field === field)[0] + const sorter = this.sorters.filter(s => s.key === field)[0] if (sorter) { - sorter.order = sorter.order === 'desc' ? 'asc' : 'desc' + sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc' } else { - this.backendSorters.push({field, order}) + this.sorters.push({key: field, dir: order}) } - this.sortingPriority = this.backendSorters + this.sortingPriority = this.sorters } else { // sort by single column only - this.backendSorters = [{field, order}] + this.sorters = [{key: field, dir: order}] this.sortingPriority = [] } @@ -629,12 +631,11 @@ sortingPriorityRemoved(field) { // prune field from active sorters - this.backendSorters = this.backendSorters.filter( - (sorter) => sorter.field !== field) + this.sorters = this.sorters.filter(s => s.key !== field) // nb. must keep active sorter list "as-is" even if // there is only one sorter; buefy seems to expect it - this.sortingPriority = this.backendSorters + this.sortingPriority = this.sorters this.loadAsyncData() }, diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 1fa0ae40..53f46020 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -345,8 +345,8 @@ class MasterView(View): self.first_visible_grid_index = grid.pager.first_item # return grid data only, if partial page was requested - if self.request.params.get('partial'): - context = {'data': grid.get_table_data()} + if self.request.GET.get('partial'): + context = grid.get_table_data() if grid.paginated and grid.paginate_on_backend: context['pager_stats'] = grid.get_vue_pager_stats() return self.json_response(context) @@ -2565,11 +2565,12 @@ class MasterView(View): so if you like you can return a different help URL depending on which type of CRUD view is in effect, etc. """ + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if info and info.help_url: @@ -2587,11 +2588,12 @@ class MasterView(View): """ Return the markdown help text for current page, if defined. """ + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if info and info.markdown_text: @@ -2608,6 +2610,8 @@ class MasterView(View): if not self.can_edit_help(): raise self.forbidden() + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -2625,13 +2629,12 @@ class MasterView(View): if not form.validate(): return {'error': "Form did not validate"} - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if not info: info = model.TailbonePageHelp(route_prefix=route_prefix) - Session.add(info) + session.add(info) info.help_url = form.validated['help_url'] info.markdown_text = form.validated['markdown_text'] @@ -2641,6 +2644,8 @@ class MasterView(View): if not self.can_edit_help(): raise self.forbidden() + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -2657,15 +2662,14 @@ class MasterView(View): if not form.validate(): return {'error': "Form did not validate"} - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailboneFieldInfo)\ + info = session.query(model.TailboneFieldInfo)\ .filter(model.TailboneFieldInfo.route_prefix == route_prefix)\ .filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\ .first() if not info: info = model.TailboneFieldInfo(route_prefix=route_prefix, field_name=form.validated['field_name']) - Session.add(info) + session.add(info) info.markdown_text = form.validated['markdown_text'] return {'ok': True} diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index c10020ea..968eaf3d 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -44,6 +44,7 @@ class PersonView(wutta.PersonView): """ model_class = Person Session = Session + sort_defaults = 'display_name' labels = { 'display_name': "Full Name", @@ -73,13 +74,6 @@ class PersonView(wutta.PersonView): # CRUD methods ############################## - def get_query(self, session=None): - """ """ - model = self.app.model - session = session or self.Session() - return session.query(model.Person)\ - .order_by(model.Person.display_name) - def configure_grid(self, g): """ """ super().configure_grid(g) diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 7cba917a..9f9b816f 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -1,6 +1,8 @@ # -*- coding: utf-8; -*- -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch + +from sqlalchemy import orm from tailbone.grids import core as mod from tests.util import WebTestCase @@ -27,6 +29,16 @@ class TestGrid(WebTestCase): grid = self.make_grid(component='blarg') self.assertEqual(grid.vue_tagname, 'blarg') + # default_sortkey, default_sortdir + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + grid = self.make_grid(default_sortkey='name') + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')]) + grid = self.make_grid(default_sortdir='desc') + self.assertEqual(grid.sort_defaults, []) + grid = self.make_grid(default_sortkey='name', default_sortdir='desc') + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + # pageable grid = self.make_grid() self.assertFalse(grid.paginated) @@ -159,6 +171,27 @@ class TestGrid(WebTestCase): grid.set_action_urls(setting, setting, 0) self.assertEqual(setting['_action_url_view'], '/blarg') + def test_default_sortkey(self): + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + self.assertIsNone(grid.default_sortkey) + grid.default_sortkey = 'name' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')]) + self.assertEqual(grid.default_sortkey, 'name') + grid.default_sortkey = 'value' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')]) + self.assertEqual(grid.default_sortkey, 'value') + + def test_default_sortdir(self): + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + self.assertIsNone(grid.default_sortdir) + self.assertRaises(ValueError, setattr, grid, 'default_sortdir', 'asc') + grid.sort_defaults = [mod.SortInfo('name', 'asc')] + grid.default_sortdir = 'desc' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + self.assertEqual(grid.default_sortdir, 'desc') + def test_pageable(self): grid = self.make_grid() self.assertFalse(grid.paginated) @@ -219,6 +252,212 @@ class TestGrid(WebTestCase): size = grid.get_pagesize() self.assertEqual(size, 15) + def test_set_sorter(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # passing None will remove sorter + self.assertIn('name', grid.sorters) + grid.set_sorter('name', None) + self.assertNotIn('name', grid.sorters) + + # can recreate sorter with just column name + grid.set_sorter('name') + self.assertIn('name', grid.sorters) + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', 'name') + self.assertIn('name', grid.sorters) + + # can recreate sorter with model property + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', model.Setting.name) + self.assertIn('name', grid.sorters) + + # extra kwargs are ignored + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', model.Setting.name, foo='bar') + self.assertIn('name', grid.sorters) + + # passing multiple args will invoke make_filter() directly + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + with patch.object(grid, 'make_sorter') as make_sorter: + make_sorter.return_value = 42 + grid.set_sorter('name', 'foo', 'bar') + make_sorter.assert_called_once_with('foo', 'bar') + self.assertEqual(grid.sorters['name'], 42) + + def test_make_simple_sorter(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # delegates to grid.make_sorter() + with patch.object(grid, 'make_sorter') as make_sorter: + make_sorter.return_value = 42 + sorter = grid.make_simple_sorter('name', foldcase=True) + make_sorter.assert_called_once_with('name', foldcase=True) + self.assertEqual(sorter, 42) + + def test_load_settings(self): + model = self.app.model + + # nb. first use a paging grid + grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True, + pagesize=20, page=1) + + # settings are loaded, applied, saved + self.assertEqual(grid.page, 1) + self.assertNotIn('grid.foo.page', self.request.session) + self.request.GET = {'pagesize': '10', 'page': '2'} + grid.load_settings() + self.assertEqual(grid.page, 2) + self.assertEqual(self.request.session['grid.foo.page'], 2) + + # can skip the saving step + self.request.GET = {'pagesize': '10', 'page': '3'} + grid.load_settings(store=False) + self.assertEqual(grid.page, 3) + self.assertEqual(self.request.session['grid.foo.page'], 2) + + # no error for non-paginated grid + grid = self.make_grid(key='foo', paginated=False) + grid.load_settings() + self.assertFalse(grid.paginated) + + # nb. next use a sorting grid + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # settings are loaded, applied, saved + self.assertEqual(grid.sort_defaults, []) + self.assertFalse(hasattr(grid, 'active_sorters')) + self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'} + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + + # can skip the saving step + self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'} + grid.load_settings(store=False) + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + + # no error for non-sortable grid + grid = self.make_grid(key='foo', sortable=False) + grid.load_settings() + self.assertFalse(grid.sortable) + + # with sort defaults + grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_on_backend=True, sort_defaults='name') + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + + # with multi-column sort defaults + grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_on_backend=True) + grid.sort_defaults = [ + mod.SortInfo('name', 'asc'), + mod.SortInfo('value', 'desc'), + ] + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + + # load settings from session when nothing is in request + self.request.GET = {} + self.request.session.invalidate() + self.assertNotIn('grid.settings.sorters.length', self.request.session) + self.request.session['grid.settings.sorters.length'] = 1 + self.request.session['grid.settings.sorters.1.key'] = 'name' + self.request.session['grid.settings.sorters.1.dir'] = 'desc' + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True, + paginated=True, paginate_on_backend=True) + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + + def test_sort_data(self): + model = self.app.model + sample_data = [ + {'name': 'foo1', 'value': 'ONE'}, + {'name': 'foo2', 'value': 'two'}, + {'name': 'foo3', 'value': 'three'}, + {'name': 'foo4', 'value': 'four'}, + {'name': 'foo5', 'value': 'five'}, + {'name': 'foo6', 'value': 'six'}, + {'name': 'foo7', 'value': 'seven'}, + {'name': 'foo8', 'value': 'eight'}, + {'name': 'foo9', 'value': 'nine'}, + ] + for setting in sample_data: + self.app.save_setting(self.session, setting['name'], setting['value']) + self.session.commit() + sample_query = self.session.query(model.Setting) + + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True, + sort_defaults=('name', 'desc')) + grid.load_settings() + + # can sort a simple list of data + sorted_data = grid.sort_data(sample_data) + self.assertIsInstance(sorted_data, list) + self.assertEqual(len(sorted_data), 9) + self.assertEqual(sorted_data[0]['name'], 'foo9') + self.assertEqual(sorted_data[-1]['name'], 'foo1') + + # can also sort a data query + sorted_query = grid.sort_data(sample_query) + self.assertIsInstance(sorted_query, orm.Query) + sorted_data = sorted_query.all() + self.assertEqual(len(sorted_data), 9) + self.assertEqual(sorted_data[0]['name'], 'foo9') + self.assertEqual(sorted_data[-1]['name'], 'foo1') + + # cannot sort data if sorter missing in overrides + sorted_data = grid.sort_data(sample_data, sorters=[]) + # nb. sorted data is in same order as original sample (not sorted) + self.assertEqual(sorted_data[0]['name'], 'foo1') + self.assertEqual(sorted_data[-1]['name'], 'foo9') + + # error if mult-column sort attempted + self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[ + {'key': 'name', 'dir': 'desc'}, + {'key': 'value', 'dir': 'asc'}, + ]) + + # cannot sort data if sortfunc is missing for column + grid.remove_sorter('name') + sorted_data = grid.sort_data(sample_data) + # nb. sorted data is in same order as original sample (not sorted) + self.assertEqual(sorted_data[0]['name'], 'foo1') + self.assertEqual(sorted_data[-1]['name'], 'foo9') + + # cannot sort data if sortfunc is missing for column + grid.remove_sorter('name') + # nb. attempting multi-column sort, but only one sorter exists + self.assertEqual(list(grid.sorters), ['value']) + grid.active_sorters = [{'key': 'name', 'dir': 'asc'}, + {'key': 'value', 'dir': 'asc'}] + with patch.object(sample_query, 'order_by') as order_by: + order_by.return_value = 42 + sorted_query = grid.sort_data(sample_query) + order_by.assert_called_once() + self.assertEqual(len(order_by.call_args.args), 1) + self.assertEqual(sorted_query, 42) + def test_render_vue_tag(self): model = self.app.model @@ -249,11 +488,13 @@ class TestGrid(WebTestCase): model = self.app.model # sanity check - grid = self.make_grid('settings', model_class=model.Setting) + grid = self.make_grid('settings', model_class=model.Setting, sortable=True) columns = grid.get_vue_columns() self.assertEqual(len(columns), 2) self.assertEqual(columns[0]['field'], 'name') + self.assertTrue(columns[0]['sortable']) self.assertEqual(columns[1]['field'], 'value') + self.assertTrue(columns[1]['sortable']) def test_get_vue_data(self): model = self.app.model diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 572875a0..0e459e7d 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -1,6 +1,6 @@ # -*- coding: utf-8; -*- -from unittest.mock import patch +from unittest.mock import patch, MagicMock from tailbone.views import master as mod from wuttaweb.grids import GridAction @@ -33,3 +33,34 @@ class TestMasterView(WebTestCase): view = self.make_view() action = view.make_action('view') self.assertIsInstance(action, GridAction) + + def test_index(self): + self.pyramid_config.include('tailbone.views.common') + self.pyramid_config.include('tailbone.views.auth') + model = self.app.model + + # mimic view for /settings + with patch.object(mod, 'Session', return_value=self.session): + with patch.multiple(mod.MasterView, create=True, + model_class=model.Setting, + Session=MagicMock(return_value=self.session), + get_index_url=MagicMock(return_value='/settings/'), + get_help_url=MagicMock(return_value=None)): + + # basic + view = self.make_view() + response = view.index() + self.assertEqual(response.status_code, 200) + + # then again with data, to include view action url + data = [{'name': 'foo', 'value': 'bar'}] + with patch.object(view, 'get_data', return_value=data): + response = view.index() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content_type, 'text/html') + + # then once more as 'partial' - aka. data only + self.request.GET = {'partial': '1'} + response = view.index() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content_type, 'application/json') From 290f8fd51eddca9e2f3778a23f44bfe356e94ad7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 19:22:04 -0500 Subject: [PATCH 010/102] feat: move multi-column grid sorting logic to wuttaweb tailbone grid template still duplicates much for Vue, and will until we can port the filters and anything else remaining.. --- tailbone/grids/core.py | 251 +++++++------------------ tailbone/templates/base.mako | 18 +- tailbone/templates/grids/complete.mako | 181 ++++++++++-------- tailbone/views/master.py | 3 +- tests/grids/test_core.py | 91 ++++++--- 5 files changed, 252 insertions(+), 292 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index cc1888fb..9c445fec 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -850,28 +850,23 @@ class Grid(WuttaGrid): return self.get_pagesize() - def load_settings(self, store=True): - """ - Load current/effective settings for the grid, from the request query - string and/or session storage. If ``store`` is true, then once - settings have been fully read, they are stored in current session for - next time. Finally, various instance attributes of the grid and its - filters are updated in-place to reflect the settings; this is so code - needn't access the settings dict directly, but the more Pythonic - instance attributes. - """ + def load_settings(self, **kwargs): + """ """ + if 'store' in kwargs: + warnings.warn("the 'store' param is deprecated for load_settings(); " + "please use the 'persist' param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('persist', kwargs.pop('store')) + + persist = kwargs.get('persist', True) # initial default settings settings = {} if self.sortable: if self.sort_defaults: - sort_defaults = self.sort_defaults - if len(sort_defaults) > 1: - log.warning("multiple sort defaults are not yet supported; " - "list will be pruned to first element for '%s' grid: %s", - self.key, sort_defaults) - sort_defaults = [sort_defaults[0]] - sortinfo = sort_defaults[0] + # nb. as of writing neither Buefy nor Oruga support a + # multi-column *default* sort; so just use first sorter + sortinfo = self.sort_defaults[0] settings['sorters.length'] = 1 settings['sorters.1.key'] = sortinfo.sortkey settings['sorters.1.dir'] = sortinfo.sortdir @@ -900,16 +895,16 @@ class Grid(WuttaGrid): elif self.filterable and self.request_has_settings('filter'): self.update_filter_settings(settings, 'request') if self.request_has_settings('sort'): - self.update_sort_settings(settings, 'request') + self.update_sort_settings(settings, src='request') else: - self.update_sort_settings(settings, 'session') + self.update_sort_settings(settings, src='session') self.update_page_settings(settings) # If request has no filter settings but does have sort settings, grab # those, then grab filter settings from session, then grab pager # settings from request or session. elif self.request_has_settings('sort'): - self.update_sort_settings(settings, 'request') + self.update_sort_settings(settings, src='request') self.update_filter_settings(settings, 'session') self.update_page_settings(settings) @@ -921,26 +916,26 @@ class Grid(WuttaGrid): elif self.request_has_settings('page'): self.update_page_settings(settings) self.update_filter_settings(settings, 'session') - self.update_sort_settings(settings, 'session') + self.update_sort_settings(settings, src='session') # If request has no settings, grab all from session. elif self.session_has_settings(): self.update_filter_settings(settings, 'session') - self.update_sort_settings(settings, 'session') + self.update_sort_settings(settings, src='session') self.update_page_settings(settings) # If no settings were found in request or session, don't store result. else: - store = False + persist = False # Maybe store settings for next time. - if store: - self.persist_settings(settings, 'session') + if persist: + self.persist_settings(settings, dest='session') # If request contained instruction to save current settings as defaults # for the current user, then do that. if self.request.GET.get('save-current-filters-as-defaults') == 'true': - self.persist_settings(settings, 'defaults') + self.persist_settings(settings, dest='defaults') # update ourself to reflect settings if self.filterable: @@ -1107,44 +1102,6 @@ class Grid(WuttaGrid): return any([key.startswith(f'{prefix}.filter') for key in self.request.session]) - def get_setting(self, source, settings, key, normalize=lambda v: v, default=None): - """ - Get the effective value for a particular setting, preferring ``source`` - but falling back to existing ``settings`` and finally the ``default``. - """ - if source not in ('request', 'session'): - raise ValueError("Invalid source identifier: {}".format(source)) - - # If source is query string, try that first. - if source == 'request': - value = self.request.GET.get(key) - if value is not None: - try: - value = normalize(value) - except ValueError: - pass - else: - return value - - # Or, if source is session, try that first. - else: - value = self.request.session.get('grid.{}.{}'.format(self.key, key)) - if value is not None: - return normalize(value) - - # If source had nothing, try default/existing settings. - value = settings.get(key) - if value is not None: - try: - value = normalize(value) - except ValueError: - pass - else: - return value - - # Okay then, default it is. - return default - def update_filter_settings(self, settings, source): """ Updates a settings dictionary according to filter settings data found @@ -1165,71 +1122,18 @@ class Grid(WuttaGrid): # consider filter active if query string contains a value for it settings['{}.active'.format(prefix)] = filtr.key in self.request.GET settings['{}.verb'.format(prefix)] = self.get_setting( - source, settings, '{}.verb'.format(filtr.key), default='') + settings, f'{filtr.key}.verb', src='request', default='') settings['{}.value'.format(prefix)] = self.get_setting( - source, settings, filtr.key, default='') + settings, filtr.key, src='request', default='') else: # source = session settings['{}.active'.format(prefix)] = self.get_setting( - source, settings, '{}.active'.format(prefix), + settings, f'{prefix}.active', src='session', normalize=lambda v: str(v).lower() == 'true', default=False) settings['{}.verb'.format(prefix)] = self.get_setting( - source, settings, '{}.verb'.format(prefix), default='') + settings, f'{prefix}.verb', src='session', default='') settings['{}.value'.format(prefix)] = self.get_setting( - source, settings, '{}.value'.format(prefix), default='') - - def update_sort_settings(self, settings, source): - """ - Updates a settings dictionary according to sort settings data found in - either the GET query string, or session storage. - - :param settings: Dictionary of initial settings, which is to be updated. - - :param source: String identifying the source to consult for settings - data. Must be one of: ``('request', 'session')``. - """ - if not self.sortable: - return - - if source == 'request': - - # TODO: remove this eventually, but some links in the wild - # may still include these params, so leave it for now - if 'sortkey' in self.request.GET: - settings['sorters.length'] = 1 - settings['sorters.1.key'] = self.get_setting(source, settings, 'sortkey') - settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir') - - else: # the future - i = 1 - while True: - skey = f'sort{i}key' - if skey in self.request.GET: - settings[f'sorters.{i}.key'] = self.get_setting(source, settings, skey) - settings[f'sorters.{i}.dir'] = self.get_setting(source, settings, f'sort{i}dir') - else: - break - i += 1 - settings['sorters.length'] = i - 1 - - else: # session - - # TODO: definitely will remove this, but leave it for now - # so it doesn't monkey with current user sessions when - # next upgrade happens. so, remove after all are upgraded - sortkey = self.get_setting(source, settings, 'sortkey') - if sortkey: - settings['sorters.length'] = 1 - settings['sorters.1.key'] = sortkey - settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir') - - else: # the future - settings['sorters.length'] = self.get_setting(source, settings, - 'sorters.length', int) - for i in range(1, settings['sorters.length'] + 1): - for key in ('key', 'dir'): - skey = f'sorters.{i}.{key}' - settings[skey] = self.get_setting(source, settings, skey) + settings, f'{prefix}.value', src='session', default='') def update_page_settings(self, settings): """ @@ -1264,18 +1168,19 @@ class Grid(WuttaGrid): if page is not None: settings['page'] = int(page) - def persist_settings(self, settings, to='session'): - """ - Persist the given settings in some way, as defined by ``func``. - """ + def persist_settings(self, settings, dest='session'): + """ """ + if dest not in ('defaults', 'session'): + raise ValueError(f"invalid dest identifier: {dest}") + app = self.request.rattail_config.get_app() model = app.model - def persist(key, value=lambda k: settings[k]): - if to == 'defaults': + def persist(key, value=lambda k: settings.get(k)): + if dest == 'defaults': skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key) app.save_setting(Session(), skey, value(key)) - else: # to == session + else: # dest == session skey = 'grid.{}.{}'.format(self.key, key) self.request.session[skey] = value(key) @@ -1287,9 +1192,11 @@ class Grid(WuttaGrid): if self.sortable: - # first clear existing settings for *sorting* only - # nb. this is because number of sort settings will vary - if to == 'defaults': + # first must clear all sort settings from dest. this is + # because number of sort settings will vary, so we delete + # all and then write all + + if dest == 'defaults': prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}' query = Session.query(model.Setting)\ .filter(sa.or_( @@ -1303,7 +1210,9 @@ class Grid(WuttaGrid): for setting in query.all(): Session.delete(setting) Session.flush() + else: # session + # remove sort settings from user session prefix = f'grid.{self.key}' for key in list(self.request.session): if key.startswith(f'{prefix}.sorters.'): @@ -1315,10 +1224,12 @@ class Grid(WuttaGrid): self.request.session.pop(f'{prefix}.sortkey', None) self.request.session.pop(f'{prefix}.sortdir', None) - persist('sorters.length') - for i in range(1, settings['sorters.length'] + 1): - persist(f'sorters.{i}.key') - persist(f'sorters.{i}.dir') + # now save sort settings to dest + if 'sorters.length' in settings: + persist('sorters.length') + for i in range(1, settings['sorters.length'] + 1): + persist(f'sorters.{i}.key') + persist(f'sorters.{i}.dir') if self.paginated: persist('pagesize') @@ -1351,58 +1262,32 @@ class Grid(WuttaGrid): if not sorters: return data - # sqlalchemy queries require special handling, in case of - # multi-column sorting - if isinstance(data, orm.Query): + # nb. when data is a query, we want to apply sorters in the + # requested order, so the final query has order_by() in the + # correct "as-is" sequence. however when data is a list we + # must do the opposite, applying in the reverse order, so the + # final list has the most "important" sort(s) applied last. + if not isinstance(data, orm.Query): + sorters = reversed(sorters) - # collect actual column sorters for order_by clause - query_sorters = [] - for sorter in sorters: - sortkey = sorter['key'] - sortdir = sorter['dir'] + for sorter in sorters: + sortkey = sorter['key'] + sortdir = sorter['dir'] - # cannot sort unless we have a sorter callable - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - log.warning("unknown sorter: %s", sorter) - continue + # cannot sort unless we have a sorter callable + sortfunc = self.sorters.get(sortkey) + if not sortfunc: + return data - # join appropriate model if needed - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) + # join appropriate model if needed + if sortkey in self.joiners and sortkey not in self.joined: + data = self.joiners[sortkey](data) + self.joined.add(sortkey) - # add column/dir to collection - query_sorters.append(getattr(sortfunc._column, sortdir)()) + # invoke the sorter + data = sortfunc(data, sortdir) - # apply sorting to query - if query_sorters: - data = data.order_by(*query_sorters) - - return data - - # manual sorting; only one column allowed - if len(sorters) != 1: - raise NotImplementedError("mulit-column manual sorting not yet supported") - - # our one and only active sorter - sorter = sorters[0] - sortkey = sorter['key'] - sortdir = sorter['dir'] - - # cannot sort unless we have a sorter callable - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data - - # apply joins needed for this sorter - # TODO: is this actually relevant for manual sort? - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) - - # invoke the sorter - return sortfunc(data, sortdir) + return data def paginate_data(self, data): """ diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 3a12859e..8e3b7785 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -658,19 +658,19 @@ ## TODO: is there a better way to check if viewing parent? % if parent_instance is Undefined: % if master.editable and instance_editable and master.has_perm('edit'): - % endif - % if master.cloneable and master.has_perm('clone'): - % endif % if master.deletable and instance_deletable and master.has_perm('delete'): - @@ -679,7 +679,7 @@ % else: ## viewing row % if instance_deletable and master.has_perm('delete_row'): - @@ -688,13 +688,13 @@ % endif % elif master and master.editing: % if master.viewable and master.has_perm('view'): - % endif % if master.deletable and instance_deletable and master.has_perm('delete'): - @@ -702,13 +702,13 @@ % endif % elif master and master.deleting: % if master.viewable and master.has_perm('view'): - % endif % if master.editable and instance_editable and master.has_perm('edit'): - diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 5a005c2e..8dc2d6dc 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -83,26 +83,29 @@ ## sorting % if grid.sortable: - ## nb. buefy only supports *one* default sorter - :default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null" - - backend-sorting - @sort="onSort" - @sorting-priority-removed="sortingPriorityRemoved" - - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - :sort-multiple="allowMultiSort" - - - ## nb. otherwise there may be default multi-column sort - :sort-multiple-data="sortingPriority" - - ## user must ctrl-click column header to do multi-sort - sort-multiple-key="ctrlKey" + ## nb. buefy/oruga only support *one* default sorter + :default-sort="sorters.length ? [sorters[0].field, sorters[0].order] : null" + % if grid.sort_on_backend: + backend-sorting + @sort="onSort" + % endif + % if grid.sort_multiple: + % if grid.sort_on_backend: + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + :sort-multiple="allowMultiSort" + :sort-multiple-data="sortingPriority" + @sorting-priority-removed="sortingPriorityRemoved" + % else: + sort-multiple + % endif + ## nb. user must ctrl-click column header for multi-sort + sort-multiple-key="ctrlKey" + % endif % endif % if getattr(grid, 'click_handlers', None): @@ -276,23 +279,24 @@ ## sorting % if grid.sortable: - sorters: ${json.dumps(grid.active_sorters)|n}, - - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - allowMultiSort: false, - - ## nb. this will only contain multi-column sorters, - ## but will be *empty* for single-column sorting - % if len(grid.active_sorters) > 1: - sortingPriority: ${json.dumps(grid.active_sorters)|n}, - % else: - sortingPriority: [], + sorters: ${json.dumps(grid.get_vue_active_sorters())|n}, + % if grid.sort_multiple: + % if grid.sort_on_backend: + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + allowMultiSort: false, + ## nb. this should be empty when current sort is single-column + % if len(grid.active_sorters) > 1: + sortingPriority: ${json.dumps(grid.get_vue_active_sorters())|n}, + % else: + sortingPriority: [], + % endif + % endif % endif - % endif ## filterable: ${json.dumps(grid.filterable)|n}, @@ -395,14 +399,19 @@ }, }, - mounted() { - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - this.allowMultiSort = true - }, + % if grid.sortable and grid.sort_multiple and grid.sort_on_backend: + + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + mounted() { + this.allowMultiSort = true + }, + + % endif methods: { @@ -483,8 +492,8 @@ } % if grid.sortable and grid.sort_on_backend: for (let i = 1; i <= this.sorters.length; i++) { - params['sort'+i+'key'] = this.sorters[i-1].key - params['sort'+i+'dir'] = this.sorters[i-1].dir + params['sort'+i+'key'] = this.sorters[i-1].field + params['sort'+i+'dir'] = this.sorters[i-1].order } % endif return params @@ -597,48 +606,66 @@ }) }, - onSort(field, order, event) { + % if grid.sortable and grid.sort_on_backend: - ## nb. buefy passes field name; oruga passes field object - % if request.use_oruga: - field = field.field - % endif + onSort(field, order, event) { - if (event.ctrlKey) { + ## nb. buefy passes field name; oruga passes field object + % if request.use_oruga: + field = field.field + % endif - // engage or enhance multi-column sorting - const sorter = this.sorters.filter(s => s.key === field)[0] - if (sorter) { - sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc' - } else { - this.sorters.push({key: field, dir: order}) - } - this.sortingPriority = this.sorters + % if grid.sort_multiple: - } else { + // did user ctrl-click the column header? + if (event.ctrlKey) { + + // toggle direction for existing, or add new sorter + const sorter = this.sorters.filter(s => s.field === field)[0] + if (sorter) { + sorter.order = sorter.order === 'desc' ? 'asc' : 'desc' + } else { + this.sorters.push({field, order}) + } + + // apply multi-column sorting + this.sortingPriority = this.sorters + + } else { + + % endif // sort by single column only - this.sorters = [{key: field, dir: order}] - this.sortingPriority = [] - } + this.sorters = [{field, order}] - // always reset to first page when changing sort options - // TODO: i mean..right? would we ever not want that? - this.currentPage = 1 - this.loadAsyncData() - }, + % if grid.sort_multiple: + // multi-column sort not engaged + this.sortingPriority = [] + } + % endif - sortingPriorityRemoved(field) { + // nb. always reset to first page when sorting changes + this.currentPage = 1 + this.loadAsyncData() + }, - // prune field from active sorters - this.sorters = this.sorters.filter(s => s.key !== field) + % if grid.sort_multiple: - // nb. must keep active sorter list "as-is" even if - // there is only one sorter; buefy seems to expect it - this.sortingPriority = this.sorters + sortingPriorityRemoved(field) { - this.loadAsyncData() - }, + // prune from active sorters + this.sorters = this.sorters.filter(s => s.field !== field) + + // nb. even though we might have just one sorter + // now, we are still technically in multi-sort mode + this.sortingPriority = this.sorters + + this.loadAsyncData() + }, + + % endif + + % endif resetView() { this.loading = true diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 53f46020..dde72106 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -341,7 +341,7 @@ class MasterView(View): return self.redirect(self.request.current_route_url(**kw)) # Stash some grid stats, for possible use when generating URLs. - if grid.pageable and hasattr(grid, 'pager'): + if grid.paginated and hasattr(grid, 'pager'): self.first_visible_grid_index = grid.pager.first_item # return grid data only, if partial page was requested @@ -442,6 +442,7 @@ class MasterView(View): 'filterable': self.filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.sortable, + 'sort_multiple': not self.request.use_oruga, 'paginated': self.pageable, 'extra_row_class': self.grid_extra_class, 'url': lambda obj: self.get_action_url('view', obj), diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 9f9b816f..c621627a 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -388,14 +388,63 @@ class TestGrid(WebTestCase): grid.load_settings() self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + def test_persist_settings(self): + model = self.app.model + + # nb. start out with paginated-only grid + grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True) + + # invalid dest + self.assertRaises(ValueError, grid.persist_settings, {}, dest='doesnotexist') + + # nb. no error if empty settings, but it saves null values + grid.persist_settings({}, dest='session') + self.assertIsNone(self.request.session['grid.foo.page']) + + # provided values are saved + grid.persist_settings({'pagesize': 15, 'page': 3}, dest='session') + self.assertEqual(self.request.session['grid.foo.page'], 3) + + # nb. now switch to sortable-only grid + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # no error if empty settings; does not save values + grid.persist_settings({}, dest='session') + self.assertNotIn('grid.settings.sorters.length', self.request.session) + + # provided values are saved + grid.persist_settings({'sorters.length': 2, + 'sorters.1.key': 'name', + 'sorters.1.dir': 'desc', + 'sorters.2.key': 'value', + 'sorters.2.dir': 'asc'}, + dest='session') + self.assertEqual(self.request.session['grid.settings.sorters.length'], 2) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + self.assertEqual(self.request.session['grid.settings.sorters.2.key'], 'value') + self.assertEqual(self.request.session['grid.settings.sorters.2.dir'], 'asc') + + # old values removed when new are saved + grid.persist_settings({'sorters.length': 1, + 'sorters.1.key': 'name', + 'sorters.1.dir': 'desc'}, + dest='session') + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + self.assertNotIn('grid.settings.sorters.2.key', self.request.session) + self.assertNotIn('grid.settings.sorters.2.dir', self.request.session) + def test_sort_data(self): model = self.app.model sample_data = [ {'name': 'foo1', 'value': 'ONE'}, {'name': 'foo2', 'value': 'two'}, - {'name': 'foo3', 'value': 'three'}, - {'name': 'foo4', 'value': 'four'}, - {'name': 'foo5', 'value': 'five'}, + {'name': 'foo3', 'value': 'ggg'}, + {'name': 'foo4', 'value': 'ggg'}, + {'name': 'foo5', 'value': 'ggg'}, {'name': 'foo6', 'value': 'six'}, {'name': 'foo7', 'value': 'seven'}, {'name': 'foo8', 'value': 'eight'}, @@ -432,32 +481,30 @@ class TestGrid(WebTestCase): self.assertEqual(sorted_data[0]['name'], 'foo1') self.assertEqual(sorted_data[-1]['name'], 'foo9') - # error if mult-column sort attempted - self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[ - {'key': 'name', 'dir': 'desc'}, - {'key': 'value', 'dir': 'asc'}, - ]) + # multi-column sorting for list data + sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) + self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'}) + + # multi-column sorting for query + sorted_query = grid.sort_data(sample_query, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) + self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'}) # cannot sort data if sortfunc is missing for column grid.remove_sorter('name') - sorted_data = grid.sort_data(sample_data) + sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) # nb. sorted data is in same order as original sample (not sorted) self.assertEqual(sorted_data[0]['name'], 'foo1') self.assertEqual(sorted_data[-1]['name'], 'foo9') - # cannot sort data if sortfunc is missing for column - grid.remove_sorter('name') - # nb. attempting multi-column sort, but only one sorter exists - self.assertEqual(list(grid.sorters), ['value']) - grid.active_sorters = [{'key': 'name', 'dir': 'asc'}, - {'key': 'value', 'dir': 'asc'}] - with patch.object(sample_query, 'order_by') as order_by: - order_by.return_value = 42 - sorted_query = grid.sort_data(sample_query) - order_by.assert_called_once() - self.assertEqual(len(order_by.call_args.args), 1) - self.assertEqual(sorted_query, 42) - def test_render_vue_tag(self): model = self.app.model From b7955a587179e4c7819a9d0a67a60be280e9c386 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 19:58:50 -0500 Subject: [PATCH 011/102] =?UTF-8?q?bump:=20version=200.18.0=20=E2=86=92=20?= =?UTF-8?q?0.19.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0671e03b..72798b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.19.0 (2024-08-18) + +### Feat + +- move multi-column grid sorting logic to wuttaweb +- move single-column grid sorting logic to wuttaweb + +### Fix + +- fix misc. errors in grid template per wuttaweb +- fix broken permission directives in web api startup + ## v0.18.0 (2024-08-16) ### Feat diff --git a/pyproject.toml b/pyproject.toml index bd4882c6..1840de77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.18.0" +version = "0.19.0" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.9.0", + "WuttaWeb>=0.10.0", "zope.sqlalchemy>=1.5", ] From 0fb3c0f3d2dde74157b77d0313756151c1373317 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:23:31 -0500 Subject: [PATCH 012/102] fix: fix broken user auth for web API app --- tailbone/api/auth.py | 12 +++++------ tailbone/app.py | 4 ---- tailbone/auth.py | 50 ++++++++++---------------------------------- 3 files changed, 16 insertions(+), 50 deletions(-) diff --git a/tailbone/api/auth.py b/tailbone/api/auth.py index 1b347b21..a710e30d 100644 --- a/tailbone/api/auth.py +++ b/tailbone/api/auth.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,8 +24,6 @@ Tailbone Web API - Auth Views """ -from rattail.db.auth import set_user_password - from cornice import Service from tailbone.api import APIView, api @@ -42,11 +40,10 @@ class AuthenticationView(APIView): This will establish a server-side web session for the user if none exists. Note that this also resets the user's session timer. """ - data = {'ok': True} + data = {'ok': True, 'permissions': []} if self.request.user: data['user'] = self.get_user_info(self.request.user) - - data['permissions'] = list(self.request.tailbone_cached_permissions) + data['permissions'] = list(self.request.user_permissions) # background color may be set per-request, by some apps if hasattr(self.request, 'background_color') and self.request.background_color: @@ -176,7 +173,8 @@ class AuthenticationView(APIView): return {'error': "The current/old password you provided is incorrect"} # okay then, set new password - set_user_password(self.request.user, data['new_password']) + auth = self.app.get_auth_handler() + auth.set_user_password(self.request.user, data['new_password']) return { 'ok': True, 'user': self.get_user_info(self.request.user), diff --git a/tailbone/app.py b/tailbone/app.py index 5e8e49d9..626c9206 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -25,19 +25,15 @@ Application Entry Point """ import os -import warnings -import sqlalchemy as sa from sqlalchemy.orm import sessionmaker, scoped_session from wuttjamaican.util import parse_list from rattail.config import make_config from rattail.exceptions import ConfigurationError -from rattail.db.types import GPCType from pyramid.config import Configurator -from pyramid.authentication import SessionAuthenticationPolicy from zope.sqlalchemy import register import tailbone.db diff --git a/tailbone/auth.py b/tailbone/auth.py index fbe6bf2f..95bf90ba 100644 --- a/tailbone/auth.py +++ b/tailbone/auth.py @@ -27,20 +27,18 @@ Authentication & Authorization import logging import re -from rattail.util import NOTSET +from wuttjamaican.util import UNSPECIFIED -from zope.interface import implementer -from pyramid.authentication import SessionAuthenticationHelper -from pyramid.request import RequestLocalCache from pyramid.security import remember, forget +from wuttaweb.auth import WuttaSecurityPolicy from tailbone.db import Session log = logging.getLogger(__name__) -def login_user(request, user, timeout=NOTSET): +def login_user(request, user, timeout=UNSPECIFIED): """ Perform the steps necessary to login the given user. Note that this returns a ``headers`` dict which you should pass to the redirect. @@ -49,7 +47,7 @@ def login_user(request, user, timeout=NOTSET): app = config.get_app() user.record_event(app.enum.USER_EVENT_LOGIN) headers = remember(request, user.uuid) - if timeout is NOTSET: + if timeout is UNSPECIFIED: timeout = session_timeout_for_user(config, user) log.debug("setting session timeout for '{}' to {}".format(user.username, timeout)) set_session_timeout(request, timeout) @@ -94,12 +92,12 @@ def set_session_timeout(request, timeout): request.session['_timeout'] = timeout or None -class TailboneSecurityPolicy: +class TailboneSecurityPolicy(WuttaSecurityPolicy): - def __init__(self, api_mode=False): + def __init__(self, db_session=None, api_mode=False, **kwargs): + kwargs['db_session'] = db_session or Session() + super().__init__(**kwargs) self.api_mode = api_mode - self.session_helper = SessionAuthenticationHelper() - self.identity_cache = RequestLocalCache(self.load_identity) def load_identity(self, request): config = request.registry.settings.get('rattail_config') @@ -115,7 +113,7 @@ class TailboneSecurityPolicy: if match: token = match.group(1) auth = app.get_auth_handler() - user = auth.authenticate_user_token(Session(), token) + user = auth.authenticate_user_token(self.db_session, token) if not user: @@ -126,36 +124,10 @@ class TailboneSecurityPolicy: # fetch user object from db model = app.model - user = Session.get(model.User, uuid) + user = self.db_session.get(model.User, uuid) if not user: return # this user is responsible for data changes in current request - Session().set_continuum_user(user) + self.db_session.set_continuum_user(user) return user - - def identity(self, request): - return self.identity_cache.get_or_create(request) - - def authenticated_userid(self, request): - user = self.identity(request) - if user is not None: - return user.uuid - - def remember(self, request, userid, **kw): - return self.session_helper.remember(request, userid, **kw) - - def forget(self, request, **kw): - return self.session_helper.forget(request, **kw) - - def permits(self, request, context, permission): - # nb. root user can do anything - if request.is_root: - return True - - config = request.registry.settings.get('rattail_config') - app = config.get_app() - auth = app.get_auth_handler() - - user = self.identity(request) - return auth.has_permission(Session(), user, permission) From b642c98d4091729ef8f957abb213d70c2c2e8fb8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:23:55 -0500 Subject: [PATCH 013/102] =?UTF-8?q?bump:=20version=200.19.0=20=E2=86=92=20?= =?UTF-8?q?0.19.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72798b30..ce64ec60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.19.1 (2024-08-19) + +### Fix + +- fix broken user auth for web API app + ## v0.19.0 (2024-08-18) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 1840de77..fa33a2df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.19.0" +version = "0.19.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 1d56a4c0d09d857f3d9276ac010743eceb8e2eac Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:53:10 -0500 Subject: [PATCH 014/102] fix: replace all occurrences of `component_studly` => `vue_component` --- tailbone/grids/core.py | 2 +- tailbone/templates/batch/index.mako | 6 +++--- .../batch/inventory/desktop_form.mako | 4 ++-- tailbone/templates/batch/pos/view.mako | 2 +- .../templates/batch/vendorcatalog/create.mako | 12 +++++------ tailbone/templates/batch/view.mako | 18 ++++++++--------- tailbone/templates/customers/view.mako | 4 ++-- tailbone/templates/custorders/items/view.mako | 6 +++--- tailbone/templates/departments/view.mako | 2 +- tailbone/templates/importing/runjob.mako | 14 ++++++------- tailbone/templates/login.mako | 8 ++++---- tailbone/templates/master/form.mako | 2 +- tailbone/templates/people/index.mako | 16 +++++++-------- tailbone/templates/poser/reports/view.mako | 6 +++--- tailbone/templates/products/batch.mako | 20 +++++++++---------- tailbone/templates/products/index.mako | 8 ++++---- .../templates/purchases/credits/index.mako | 12 +++++------ tailbone/templates/receiving/view.mako | 8 ++++---- .../templates/reports/generated/delete.mako | 2 +- .../templates/reports/generated/view.mako | 2 +- tailbone/templates/reports/problems/view.mako | 2 +- tailbone/templates/roles/view.mako | 2 +- tailbone/templates/settings/email/index.mako | 8 ++++---- .../templates/tempmon/appliances/view.mako | 2 +- tailbone/templates/tempmon/clients/view.mako | 2 +- .../trainwreck/transactions/view.mako | 2 +- .../trainwreck/transactions/view_row.mako | 2 +- tailbone/templates/users/view.mako | 2 +- 28 files changed, 88 insertions(+), 88 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 9c445fec..d00a85ae 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1697,7 +1697,7 @@ class Grid(WuttaGrid): results['checked_rows'] = checked # TODO: this seems a bit hacky, but is required for now to # initialize things on the client side... - var = '{}CurrentData'.format(self.component_studly) + var = '{}CurrentData'.format(self.vue_component) results['checked_rows_code'] = '[{}]'.format( ', '.join(['{}[{}]'.format(var, i) for i in checked])) diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index 209fbb0c..a7808590 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -83,7 +83,7 @@ % if master.results_executable and master.has_perm('execute_multiple'): % endif diff --git a/tailbone/templates/batch/inventory/desktop_form.mako b/tailbone/templates/batch/inventory/desktop_form.mako index 7e4795a8..8ca32ce0 100644 --- a/tailbone/templates/batch/inventory/desktop_form.mako +++ b/tailbone/templates/batch/inventory/desktop_form.mako @@ -147,7 +147,7 @@ diff --git a/tailbone/templates/batch/vendorcatalog/create.mako b/tailbone/templates/batch/vendorcatalog/create.mako index d25c8f16..63865bd5 100644 --- a/tailbone/templates/batch/vendorcatalog/create.mako +++ b/tailbone/templates/batch/vendorcatalog/create.mako @@ -5,12 +5,12 @@ ${parent.modify_this_page_vars()} % endif @@ -356,8 +356,8 @@ % endif diff --git a/tailbone/templates/customers/view.mako b/tailbone/templates/customers/view.mako index 8b07bdb3..bbca9580 100644 --- a/tailbone/templates/customers/view.mako +++ b/tailbone/templates/customers/view.mako @@ -21,10 +21,10 @@ diff --git a/tailbone/templates/importing/runjob.mako b/tailbone/templates/importing/runjob.mako index 2bc2a4e9..23526ed2 100644 --- a/tailbone/templates/importing/runjob.mako +++ b/tailbone/templates/importing/runjob.mako @@ -67,21 +67,21 @@ ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index d18323b5..f898660f 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -60,19 +60,19 @@ <%def name="modify_this_page_vars()"> % endif diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index a4a4d503..66e38028 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -22,7 +22,7 @@ <%def name="render_form_innards()"> - ${h.form(request.current_route_url(), **{'@submit': 'submit{}'.format(form.component_studly)})} + ${h.form(request.current_route_url(), **{'@submit': 'submit{}'.format(form.vue_component)})} ${h.csrf_token(request)}
@@ -43,8 +43,8 @@
- {{ ${form.component_studly}ButtonText }} + :disabled="${form.vue_component}Submitting"> + {{ ${form.vue_component}ButtonText }} Cancel @@ -66,21 +66,21 @@ ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) - let ${form.component_studly} = { + let ${form.vue_component} = { template: '#${form.component}-template', methods: { ## TODO: deprecate / remove the latter option here % if form.auto_disable_save or form.auto_disable: - submit${form.component_studly}() { - this.${form.component_studly}Submitting = true - this.${form.component_studly}ButtonText = "Working, please wait..." + submit${form.vue_component}() { + this.${form.vue_component}Submitting = true + this.${form.vue_component}ButtonText = "Working, please wait..." } % endif } } - let ${form.component_studly}Data = { + let ${form.vue_component}Data = { ## TODO: ugh, this seems pretty hacky. need to declare some data models ## for various field components to bind to... @@ -95,8 +95,8 @@ ## TODO: deprecate / remove the latter option here % if form.auto_disable_save or form.auto_disable: - ${form.component_studly}Submitting: false, - ${form.component_studly}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n}, + ${form.vue_component}Submitting: false, + ${form.vue_component}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n}, % endif ## TODO: more hackiness, this is for the sake of batch params diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index 0d4bc410..b4731dee 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -41,11 +41,11 @@ % if label_profiles and master.has_perm('print_labels'): diff --git a/tailbone/templates/reports/generated/view.mako b/tailbone/templates/reports/generated/view.mako index 6260efba..e5bcc9e4 100644 --- a/tailbone/templates/reports/generated/view.mako +++ b/tailbone/templates/reports/generated/view.mako @@ -28,7 +28,7 @@ diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako index 026c73dc..1d5cb14f 100644 --- a/tailbone/templates/reports/problems/view.mako +++ b/tailbone/templates/reports/problems/view.mako @@ -67,7 +67,7 @@ diff --git a/tailbone/templates/tempmon/clients/view.mako b/tailbone/templates/tempmon/clients/view.mako index cff22fed..b1db423b 100644 --- a/tailbone/templates/tempmon/clients/view.mako +++ b/tailbone/templates/tempmon/clients/view.mako @@ -26,7 +26,7 @@ ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/trainwreck/transactions/view.mako b/tailbone/templates/trainwreck/transactions/view.mako index 2be51c7d..02950941 100644 --- a/tailbone/templates/trainwreck/transactions/view.mako +++ b/tailbone/templates/trainwreck/transactions/view.mako @@ -6,7 +6,7 @@ diff --git a/tailbone/templates/trainwreck/transactions/view_row.mako b/tailbone/templates/trainwreck/transactions/view_row.mako index 9abcb8ba..9c76f7bd 100644 --- a/tailbone/templates/trainwreck/transactions/view_row.mako +++ b/tailbone/templates/trainwreck/transactions/view_row.mako @@ -6,7 +6,7 @@ diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako index ed2b5f16..06087927 100644 --- a/tailbone/templates/users/view.mako +++ b/tailbone/templates/users/view.mako @@ -81,7 +81,7 @@ % if master.has_perm('manage_api_tokens'): + % if getattr(master, 'has_rows', False): + ${rows_grid.render_vue_finalize()} + % endif + % if expose_versions: + ${versions_grid.render_vue_finalize()} + % endif diff --git a/tailbone/views/master.py b/tailbone/views/master.py index dde72106..ac74a070 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -347,8 +347,6 @@ class MasterView(View): # return grid data only, if partial page was requested if self.request.GET.get('partial'): context = grid.get_table_data() - if grid.paginated and grid.paginate_on_backend: - context['pager_stats'] = grid.get_vue_pager_stats() return self.json_response(context) context = { @@ -587,7 +585,8 @@ class MasterView(View): 'filterable': self.rows_filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.rows_sortable, - 'pageable': self.rows_pageable, + 'sort_multiple': not self.request.use_oruga, + 'paginated': self.rows_pageable, 'extra_row_class': self.row_grid_extra_class, 'url': lambda obj: self.get_row_action_url('view', obj), } @@ -675,7 +674,7 @@ class MasterView(View): defaults = { 'model_class': continuum.transaction_class(self.get_model_class()), 'width': 'full', - 'pageable': True, + 'paginated': True, 'url': lambda txn: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id), } if 'actions' not in kwargs: @@ -1387,8 +1386,8 @@ class MasterView(View): 'vue_tagname': 'versions-grid', 'ajax_data_url': self.get_action_url('revisions_data', obj), 'sortable': True, - 'default_sortkey': 'changed', - 'default_sortdir': 'desc', + 'sort_multiple': not self.request.use_oruga, + 'sort_defaults': ('changed', 'desc'), 'actions': [ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), From b762a0782a1b677817166609ee8b94bca872a7e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 13:57:36 -0500 Subject: [PATCH 019/102] =?UTF-8?q?bump:=20version=200.19.2=20=E2=86=92=20?= =?UTF-8?q?0.19.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe71f3f..c8017445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.19.3 (2024-08-19) + +### Fix + +- add pager stats to all grid vue data (fixes view history) + ## v0.19.2 (2024-08-19) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 8f840642..3e07abaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.19.2" +version = "0.19.3" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.10.1", + "WuttaWeb>=0.10.2", "zope.sqlalchemy>=1.5", ] From d29b8403435237effd5ca2d122a9fb00ff6896b2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 14:38:41 -0500 Subject: [PATCH 020/102] fix: avoid deprecated reference to app db engine --- tailbone/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/app.py b/tailbone/app.py index 626c9206..ad9663cf 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -63,8 +63,8 @@ def make_rattail_config(settings): settings['wutta_config'] = rattail_config # configure database sessions - if hasattr(rattail_config, 'rattail_engine'): - tailbone.db.Session.configure(bind=rattail_config.rattail_engine) + if hasattr(rattail_config, 'appdb_engine'): + tailbone.db.Session.configure(bind=rattail_config.appdb_engine) if hasattr(rattail_config, 'trainwreck_engine'): tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine) if hasattr(rattail_config, 'tempmon_engine'): From 1ec1eba49681867aac1e24e11d3b89ed8bba060e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 21:30:58 -0500 Subject: [PATCH 021/102] feat: refactor templates to simplify base/page/form structure to mimic what has been done in wuttaweb --- tailbone/templates/appinfo/configure.mako | 9 +- tailbone/templates/appinfo/index.mako | 11 +- tailbone/templates/appsettings.mako | 20 +- tailbone/templates/base.mako | 164 +++++---- tailbone/templates/batch/index.mako | 36 +- .../batch/inventory/desktop_form.mako | 11 +- tailbone/templates/batch/pos/view.mako | 10 +- .../batch/vendorcatalog/configure.mako | 11 +- .../templates/batch/vendorcatalog/create.mako | 9 +- tailbone/templates/batch/view.mako | 58 ++-- tailbone/templates/configure-menus.mako | 9 +- tailbone/templates/configure.mako | 9 +- tailbone/templates/customers/configure.mako | 9 +- .../templates/customers/pending/view.mako | 8 +- tailbone/templates/customers/view.mako | 8 +- tailbone/templates/custorders/create.mako | 18 +- tailbone/templates/custorders/items/view.mako | 8 +- .../templates/datasync/changes/index.mako | 9 +- tailbone/templates/datasync/configure.mako | 9 +- tailbone/templates/datasync/status.mako | 8 +- tailbone/templates/departments/view.mako | 10 +- tailbone/templates/form.mako | 20 +- tailbone/templates/generate_feature.mako | 9 +- tailbone/templates/importing/configure.mako | 9 +- tailbone/templates/importing/runjob.mako | 8 +- tailbone/templates/login.mako | 8 +- tailbone/templates/luigi/configure.mako | 9 +- tailbone/templates/luigi/index.mako | 9 +- tailbone/templates/master/clone.mako | 9 +- tailbone/templates/master/delete.mako | 7 +- tailbone/templates/master/form.mako | 9 +- tailbone/templates/master/index.mako | 44 +-- tailbone/templates/master/merge.mako | 23 +- tailbone/templates/master/versions.mako | 31 +- tailbone/templates/master/view.mako | 54 ++- tailbone/templates/members/configure.mako | 9 +- tailbone/templates/messages/create.mako | 13 +- tailbone/templates/messages/index.mako | 17 +- tailbone/templates/messages/view.mako | 15 +- tailbone/templates/ordering/view.mako | 21 +- tailbone/templates/ordering/worksheet.mako | 25 +- tailbone/templates/page.mako | 96 +++--- tailbone/templates/people/index.mako | 8 +- .../templates/people/merge-requests/view.mako | 8 +- tailbone/templates/people/view.mako | 30 +- tailbone/templates/people/view_profile.mako | 317 +++++++++--------- tailbone/templates/poser/reports/view.mako | 20 +- tailbone/templates/poser/setup.mako | 11 +- .../templates/principal/find_by_perm.mako | 53 ++- tailbone/templates/products/batch.mako | 9 +- tailbone/templates/products/configure.mako | 9 +- tailbone/templates/products/index.mako | 9 +- tailbone/templates/products/pending/view.mako | 23 +- tailbone/templates/products/view.mako | 9 +- .../templates/purchases/credits/index.mako | 9 +- tailbone/templates/receiving/view.mako | 26 +- tailbone/templates/receiving/view_row.mako | 9 +- .../templates/reports/generated/choose.mako | 13 +- .../templates/reports/generated/delete.mako | 11 +- .../templates/reports/generated/view.mako | 11 +- tailbone/templates/reports/inventory.mako | 11 +- tailbone/templates/reports/ordering.mako | 9 +- tailbone/templates/reports/problems/view.mako | 9 +- tailbone/templates/roles/create.mako | 12 +- tailbone/templates/roles/edit.mako | 12 +- tailbone/templates/roles/view.mako | 8 +- .../templates/settings/email/configure.mako | 9 +- tailbone/templates/settings/email/index.mako | 8 +- tailbone/templates/settings/email/view.mako | 21 +- tailbone/templates/tables/create.mako | 9 +- .../templates/tempmon/appliances/view.mako | 11 +- tailbone/templates/tempmon/clients/view.mako | 11 +- tailbone/templates/tempmon/dashboard.mako | 9 +- tailbone/templates/tempmon/probes/graph.mako | 9 +- .../templates/themes/butterball/base.mako | 100 ++++-- .../trainwreck/transactions/configure.mako | 11 +- .../trainwreck/transactions/rollover.mako | 11 +- .../trainwreck/transactions/view.mako | 10 +- .../trainwreck/transactions/view_row.mako | 11 +- .../templates/units-of-measure/index.mako | 19 +- tailbone/templates/upgrades/configure.mako | 9 +- tailbone/templates/upgrades/view.mako | 21 +- tailbone/templates/users/preferences.mako | 11 +- tailbone/templates/users/view.mako | 9 +- tailbone/templates/vendors/configure.mako | 11 +- tailbone/templates/views/model/create.mako | 9 +- tailbone/templates/workorders/view.mako | 9 +- 87 files changed, 818 insertions(+), 1045 deletions(-) diff --git a/tailbone/templates/appinfo/configure.mako b/tailbone/templates/appinfo/configure.mako index aab180c4..4794f00b 100644 --- a/tailbone/templates/appinfo/configure.mako +++ b/tailbone/templates/appinfo/configure.mako @@ -213,9 +213,9 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako index 73f53920..68244300 100644 --- a/tailbone/templates/appinfo/index.mako +++ b/tailbone/templates/appinfo/index.mako @@ -114,14 +114,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/appsettings.mako b/tailbone/templates/appsettings.mako index 4f935956..ba667e0e 100644 --- a/tailbone/templates/appsettings.mako +++ b/tailbone/templates/appsettings.mako @@ -15,8 +15,8 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - - -${parent.body()} diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 8e3b7785..a0e58e22 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -34,17 +34,21 @@ - ${declare_formposter_mixin()} - - ${self.body()} - -
+
- ${self.render_whole_page_template()} - ${self.make_whole_page_component()} - ${self.make_whole_page_app()} + ## TODO: this must come before the self.body() call..but why? + ${declare_formposter_mixin()} + + ## content body from derived/child template + ${self.body()} + + ## Vue app + ${self.render_vue_templates()} + ${self.modify_vue_vars()} + ${self.make_vue_components()} + ${self.make_vue_app()} @@ -181,7 +185,7 @@ <%def name="head_tags()"> -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> -<%def name="modify_whole_page_vars()"> - - - -<%def name="finalize_whole_page_vars()"> - ## NOTE: if you override this, must use - - -<%def name="make_whole_page_app()"> - - - <%def name="wtfield(form, name, **kwargs)">
@@ -961,3 +911,87 @@
+ +############################## +## vue components + app +############################## + +<%def name="render_vue_templates()"> + ${page_help.declare_vars()} + ${multi_file_upload.declare_vars()} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))} + + ## DEPRECATED; called for back-compat + ${self.render_whole_page_template()} + + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.declare_whole_page_vars()} + + +## DEPRECATED; remains for back-compat +<%def name="declare_whole_page_vars()"> + ${self.render_vue_script_whole_page()} + + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat + ${self.modify_whole_page_vars()} + + +## DEPRECATED; remains for back-compat +<%def name="modify_whole_page_vars()"> + + + +<%def name="make_vue_components()"> + ${make_grid_filter_components()} + ${page_help.make_component()} + ${multi_file_upload.make_component()} + + + ## DEPRECATED; called for back-compat + ${self.finalize_whole_page_vars()} + ${self.make_whole_page_component()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> + + + +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_app()"> + + + +############################## +## DEPRECATED +############################## + +<%def name="finalize_whole_page_vars()"> diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index a7808590..a1b11b89 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -64,10 +64,17 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + % if master.results_executable and master.has_perm('execute_multiple'): + ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + % endif + + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.results_refreshable and master.has_perm('refresh'): - % endif % if master.results_executable and master.has_perm('execute_multiple'): - % endif - -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - % if master.results_executable and master.has_perm('execute_multiple'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} - % endif - - - -${parent.body()} diff --git a/tailbone/templates/batch/inventory/desktop_form.mako b/tailbone/templates/batch/inventory/desktop_form.mako index 8ca32ce0..cddaa2c5 100644 --- a/tailbone/templates/batch/inventory/desktop_form.mako +++ b/tailbone/templates/batch/inventory/desktop_form.mako @@ -297,14 +297,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/batch/pos/view.mako b/tailbone/templates/batch/pos/view.mako index bdb8709d..5ecabd4d 100644 --- a/tailbone/templates/batch/pos/view.mako +++ b/tailbone/templates/batch/pos/view.mako @@ -1,13 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/batch/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/batch/vendorcatalog/configure.mako b/tailbone/templates/batch/vendorcatalog/configure.mako index 0d57053e..4f91cb02 100644 --- a/tailbone/templates/batch/vendorcatalog/configure.mako +++ b/tailbone/templates/batch/vendorcatalog/configure.mako @@ -39,14 +39,9 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/batch/vendorcatalog/create.mako b/tailbone/templates/batch/vendorcatalog/create.mako index 63865bd5..d9d62bd1 100644 --- a/tailbone/templates/batch/vendorcatalog/create.mako +++ b/tailbone/templates/batch/vendorcatalog/create.mako @@ -1,9 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/batch/create.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index bef18cd4..cdfa9ba7 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -149,12 +149,6 @@ -<%def name="render_form_template()"> - ## TODO: should use self.render_form_buttons() - ## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n} - ${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n} - - <%def name="render_this_page()"> ${parent.render_this_page()} @@ -197,16 +191,6 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - ${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n} - % endif - % if master.handler.executable(batch) and master.has_perm('execute'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} - % endif - - <%def name="render_form()">
<${form.component} @show-upload="showUploadDialog = true"> @@ -267,9 +251,27 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - % endif - % if execute_enabled and master.has_perm('execute'): - % endif - - -${parent.body()} diff --git a/tailbone/templates/configure-menus.mako b/tailbone/templates/configure-menus.mako index c0200912..c7f46d21 100644 --- a/tailbone/templates/configure-menus.mako +++ b/tailbone/templates/configure-menus.mako @@ -208,9 +208,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index f33779c8..272aadce 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -205,9 +205,9 @@ ${h.end_form()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/customers/configure.mako b/tailbone/templates/customers/configure.mako index e68f4543..1a6dca8b 100644 --- a/tailbone/templates/customers/configure.mako +++ b/tailbone/templates/customers/configure.mako @@ -88,9 +88,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/customers/pending/view.mako b/tailbone/templates/customers/pending/view.mako index e9e54c99..1cea9d1f 100644 --- a/tailbone/templates/customers/pending/view.mako +++ b/tailbone/templates/customers/pending/view.mako @@ -106,9 +106,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/customers/view.mako b/tailbone/templates/customers/view.mako index bbca9580..490e4757 100644 --- a/tailbone/templates/customers/view.mako +++ b/tailbone/templates/customers/view.mako @@ -16,9 +16,9 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index 63505422..382a121f 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -47,10 +47,9 @@
-<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} ${product_lookup.tailbone_product_lookup_template()} - - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - ${product_lookup.tailbone_product_lookup_component()} - - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${product_lookup.tailbone_product_lookup_component()} + diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako index 8eaee69a..4cc92bbf 100644 --- a/tailbone/templates/custorders/items/view.mako +++ b/tailbone/templates/custorders/items/view.mako @@ -291,9 +291,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/datasync/changes/index.mako b/tailbone/templates/datasync/changes/index.mako index 6d171619..86f5c121 100644 --- a/tailbone/templates/datasync/changes/index.mako +++ b/tailbone/templates/datasync/changes/index.mako @@ -26,9 +26,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/datasync/configure.mako b/tailbone/templates/datasync/configure.mako index 7922d189..3651d0c4 100644 --- a/tailbone/templates/datasync/configure.mako +++ b/tailbone/templates/datasync/configure.mako @@ -599,9 +599,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/datasync/status.mako b/tailbone/templates/datasync/status.mako index c782dec6..e14686f8 100644 --- a/tailbone/templates/datasync/status.mako +++ b/tailbone/templates/datasync/status.mako @@ -115,8 +115,9 @@ -<%def name="modify_this_page_vars()"> - - - -${parent.body()} diff --git a/tailbone/templates/departments/view.mako b/tailbone/templates/departments/view.mako index f892f333..c5c39cbb 100644 --- a/tailbone/templates/departments/view.mako +++ b/tailbone/templates/departments/view.mako @@ -1,13 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index fec721fd..3bb04257 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -90,15 +90,15 @@ <%def name="before_object_helpers()"> -<%def name="render_this_page_template()"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if form is not Undefined: ${self.render_form_template()} % endif - ${parent.render_this_page_template()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if main_form_collapsible: % endif - - -${parent.body()} diff --git a/tailbone/templates/generate_feature.mako b/tailbone/templates/generate_feature.mako index 18a26f58..0f2a9f7b 100644 --- a/tailbone/templates/generate_feature.mako +++ b/tailbone/templates/generate_feature.mako @@ -276,9 +276,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/importing/configure.mako b/tailbone/templates/importing/configure.mako index 0396745a..2445341d 100644 --- a/tailbone/templates/importing/configure.mako +++ b/tailbone/templates/importing/configure.mako @@ -144,9 +144,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/importing/runjob.mako b/tailbone/templates/importing/runjob.mako index 23526ed2..a9625bc3 100644 --- a/tailbone/templates/importing/runjob.mako +++ b/tailbone/templates/importing/runjob.mako @@ -63,9 +63,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index f898660f..3eb46403 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -57,8 +57,9 @@ -<%def name="modify_this_page_vars()"> - - - -${parent.body()} diff --git a/tailbone/templates/luigi/configure.mako b/tailbone/templates/luigi/configure.mako index 49060ceb..de364828 100644 --- a/tailbone/templates/luigi/configure.mako +++ b/tailbone/templates/luigi/configure.mako @@ -297,9 +297,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/luigi/index.mako b/tailbone/templates/luigi/index.mako index b5134c25..0dd72d01 100644 --- a/tailbone/templates/luigi/index.mako +++ b/tailbone/templates/luigi/index.mako @@ -255,9 +255,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/master/clone.mako b/tailbone/templates/master/clone.mako index 59d6aea2..4c7e4662 100644 --- a/tailbone/templates/master/clone.mako +++ b/tailbone/templates/master/clone.mako @@ -34,9 +34,9 @@ ${h.end_form()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/master/delete.mako b/tailbone/templates/master/delete.mako index c6187d55..d2f517d9 100644 --- a/tailbone/templates/master/delete.mako +++ b/tailbone/templates/master/delete.mako @@ -33,8 +33,8 @@ ${h.end_form()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} - - -${parent.body()} diff --git a/tailbone/templates/master/form.mako b/tailbone/templates/master/form.mako index fac18ee2..17063c21 100644 --- a/tailbone/templates/master/form.mako +++ b/tailbone/templates/master/form.mako @@ -1,9 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/form.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%def name="render_this_page()"> - ${self.page_content()} +## DEPRECATED; remains for back-compat +<%def name="make_grid_component()"> + ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + diff --git a/tailbone/templates/master/merge.mako b/tailbone/templates/master/merge.mako index 5d90043f..487d258d 100644 --- a/tailbone/templates/master/merge.mako +++ b/tailbone/templates/master/merge.mako @@ -109,8 +109,8 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + diff --git a/tailbone/templates/master/versions.mako b/tailbone/templates/master/versions.mako index 307674b8..a6bb14f0 100644 --- a/tailbone/templates/master/versions.mako +++ b/tailbone/templates/master/versions.mako @@ -16,27 +16,16 @@ ${self.page_content()} -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - - -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - - ## TODO: stop using |n filter - ${grid.render_complete()|n} - - <%def name="page_content()"> - - + ${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})} -${parent.body()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${grid.render_vue_template()} + + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${grid.render_vue_finalize()} + diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index 37f57237..0a1f9c62 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -238,21 +238,34 @@ ${rows_grid.render_vue_tag(id='rowGrid', ref='rowGrid')} -<%def name="render_this_page_template()"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if getattr(master, 'has_rows', False): ${rows_grid.render_vue_template(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))} % endif - ${parent.render_this_page_template()} % if expose_versions: ${versions_grid.render_vue_template()} % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - % if expose_versions: - - % endif - - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - -<%def name="finalize_this_page_vars()"> - ${parent.finalize_this_page_vars()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if getattr(master, 'has_rows', False): ${rows_grid.render_vue_finalize()} % endif @@ -342,6 +333,3 @@ ${versions_grid.render_vue_finalize()} % endif - - -${parent.body()} diff --git a/tailbone/templates/members/configure.mako b/tailbone/templates/members/configure.mako index 465bf611..f1f0e39f 100644 --- a/tailbone/templates/members/configure.mako +++ b/tailbone/templates/members/configure.mako @@ -52,9 +52,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/messages/create.mako b/tailbone/templates/messages/create.mako index 4a15573b..39236f75 100644 --- a/tailbone/templates/messages/create.mako +++ b/tailbone/templates/messages/create.mako @@ -32,14 +32,14 @@ % endif -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} ${message_recipients_template()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/messages/index.mako b/tailbone/templates/messages/index.mako index 3fc82fd3..eaa4b6c9 100644 --- a/tailbone/templates/messages/index.mako +++ b/tailbone/templates/messages/index.mako @@ -22,15 +22,15 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if request.matched_route.name in ('messages.inbox', 'messages.archive'): - % endif - - -${parent.body()} diff --git a/tailbone/templates/messages/view.mako b/tailbone/templates/messages/view.mako index 2e2baa60..36418698 100644 --- a/tailbone/templates/messages/view.mako +++ b/tailbone/templates/messages/view.mako @@ -82,22 +82,19 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/ordering/view.mako b/tailbone/templates/ordering/view.mako index aed6fd75..584559c1 100644 --- a/tailbone/templates/ordering/view.mako +++ b/tailbone/templates/ordering/view.mako @@ -21,8 +21,8 @@ % endif -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if not batch.executed and not batch.complete and master.has_perm('edit_row'): % endif - - -${parent.body()} diff --git a/tailbone/templates/ordering/worksheet.mako b/tailbone/templates/ordering/worksheet.mako index ca1abf6e..cb98c48f 100644 --- a/tailbone/templates/ordering/worksheet.mako +++ b/tailbone/templates/ordering/worksheet.mako @@ -199,9 +199,8 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - -############################## -## page body -############################## - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + diff --git a/tailbone/templates/page.mako b/tailbone/templates/page.mako index 17d87c9a..54b47278 100644 --- a/tailbone/templates/page.mako +++ b/tailbone/templates/page.mako @@ -1,42 +1,26 @@ ## -*- coding: utf-8; -*- <%inherit file="/base.mako" /> -<%def name="context_menu_items()"> - % if context_menu_list_items is not Undefined: - % for item in context_menu_list_items: -
  • ${item}
  • - % endfor - % endif +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${self.render_vue_template_this_page()} -<%def name="page_content()"> - -<%def name="render_this_page()"> -
    - -
    - ${self.page_content()} -
    - -
      - ${self.context_menu_items()} -
    - -
    +<%def name="render_vue_template_this_page()"> + ## DEPRECATED; called for back-compat + ${self.render_this_page_template()} <%def name="render_this_page_template()"> - + -<%def name="modify_this_page_vars()"> - ## NOTE: if you override this, must use +############################## +## DEPRECATED +############################## -${self.render_this_page_template()} -${self.make_this_page_component()} +<%def name="declare_this_page_vars()"> + +<%def name="modify_this_page_vars()"> + +<%def name="finalize_this_page_vars()"> diff --git a/tailbone/templates/people/index.mako b/tailbone/templates/people/index.mako index 6ce14633..cd6fddf1 100644 --- a/tailbone/templates/people/index.mako +++ b/tailbone/templates/people/index.mako @@ -61,9 +61,9 @@ ${parent.grid_tools()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/people/merge-requests/view.mako b/tailbone/templates/people/merge-requests/view.mako index 9e8905cf..e2db1476 100644 --- a/tailbone/templates/people/merge-requests/view.mako +++ b/tailbone/templates/people/merge-requests/view.mako @@ -18,10 +18,10 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if not instance.merged and request.has_perm('people.merge'): - % endif - -${parent.body()} diff --git a/tailbone/templates/people/view.mako b/tailbone/templates/people/view.mako index d28d7558..15c669fa 100644 --- a/tailbone/templates/people/view.mako +++ b/tailbone/templates/people/view.mako @@ -2,6 +2,16 @@ <%inherit file="/master/view.mako" /> <%namespace file="/util.mako" import="view_profiles_helper" /> +<%def name="page_content()"> + ${parent.page_content()} + % if not instance.users and request.has_perm('users.create'): + ${h.form(url('people.make_user'), ref='makeUserForm')} + ${h.csrf_token(request)} + ${h.hidden('person_uuid', value=instance.uuid)} + ${h.end_form()} + % endif + + <%def name="object_helpers()"> ${parent.object_helpers()} ${view_profiles_helper([instance])} @@ -13,9 +23,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -<%def name="page_content()"> - ${parent.page_content()} - % if not instance.users and request.has_perm('users.create'): - ${h.form(url('people.make_user'), ref='makeUserForm')} - ${h.csrf_token(request)} - ${h.hidden('person_uuid', value=instance.uuid)} - ${h.end_form()} - % endif - - - -${parent.body()} - diff --git a/tailbone/templates/people/view_profile.mako b/tailbone/templates/people/view_profile.mako index cdb6c5cc..6ca5a84c 100644 --- a/tailbone/templates/people/view_profile.mako +++ b/tailbone/templates/people/view_profile.mako @@ -1966,30 +1966,97 @@ - + <%def name="declare_personal_tab_vars()"> @@ -3022,114 +3089,46 @@ -<%def name="declare_profile_info_vars()"> - - - <%def name="make_profile_info_component()"> - ${self.declare_profile_info_vars()} - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - ${self.make_personal_tab_component()} - - % if expose_members: - ${self.make_member_tab_component()} - % endif - - ${self.make_customer_tab_component()} - % if expose_customer_shoppers: - ${self.make_shopper_tab_component()} - % endif - ${self.make_employee_tab_component()} - ${self.make_notes_tab_component()} - - % if expose_transactions: - - ${self.make_transactions_tab_component()} - % endif - - ${self.make_user_tab_component()} - ${self.make_profile_info_component()} - - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - - % if request.has_perm('people_profile.view_versions'): - - % endif + % endif + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} -${parent.body()} + ${self.make_personal_tab_component()} + + % if expose_members: + ${self.make_member_tab_component()} + % endif + + ${self.make_customer_tab_component()} + % if expose_customer_shoppers: + ${self.make_shopper_tab_component()} + % endif + ${self.make_employee_tab_component()} + ${self.make_notes_tab_component()} + + % if expose_transactions: + + ${self.make_transactions_tab_component()} + % endif + + ${self.make_user_tab_component()} + ${self.make_profile_info_component()} + + +############################## +## DEPRECATED +############################## + +<%def name="declare_profile_info_vars()"> diff --git a/tailbone/templates/poser/reports/view.mako b/tailbone/templates/poser/reports/view.mako index 274a8806..cb8b51aa 100644 --- a/tailbone/templates/poser/reports/view.mako +++ b/tailbone/templates/poser/reports/view.mako @@ -62,19 +62,13 @@
    -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('replace'): - + % endif - -${parent.body()} diff --git a/tailbone/templates/poser/setup.mako b/tailbone/templates/poser/setup.mako index 8d01bb33..239e7db2 100644 --- a/tailbone/templates/poser/setup.mako +++ b/tailbone/templates/poser/setup.mako @@ -118,14 +118,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/principal/find_by_perm.mako b/tailbone/templates/principal/find_by_perm.mako index 2ea289c8..ddc44e3d 100644 --- a/tailbone/templates/principal/find_by_perm.mako +++ b/tailbone/templates/principal/find_by_perm.mako @@ -10,8 +10,16 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="principal_table()"> +
    + ${grid.render_table_element(data_prop='principalsData')|n} +
    + + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="principal_table()"> -
    - ${grid.render_table_element(data_prop='principalsData')|n} -
    - - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + + -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index 66e38028..9f969468 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -60,9 +60,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/products/configure.mako b/tailbone/templates/products/configure.mako index 6121af67..a43a85d4 100644 --- a/tailbone/templates/products/configure.mako +++ b/tailbone/templates/products/configure.mako @@ -95,9 +95,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index b4731dee..5ffa9512 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -36,10 +36,10 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if label_profiles and master.has_perm('print_labels'): - % endif - - -${parent.body()} diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako index 765c8838..72c9c76d 100644 --- a/tailbone/templates/products/pending/view.mako +++ b/tailbone/templates/products/pending/view.mako @@ -2,11 +2,6 @@ <%inherit file="/master/view.mako" /> <%namespace name="product_lookup" file="/products/lookup.mako" /> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - ${product_lookup.tailbone_product_lookup_template()} - - <%def name="page_content()"> ${parent.page_content()} @@ -67,9 +62,14 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} ${product_lookup.tailbone_product_lookup_component()} - - -${parent.body()} diff --git a/tailbone/templates/products/view.mako b/tailbone/templates/products/view.mako index bd4afc7f..66ca3128 100644 --- a/tailbone/templates/products/view.mako +++ b/tailbone/templates/products/view.mako @@ -282,9 +282,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/purchases/credits/index.mako b/tailbone/templates/purchases/credits/index.mako index 0cfbc031..94028bdb 100644 --- a/tailbone/templates/purchases/credits/index.mako +++ b/tailbone/templates/purchases/credits/index.mako @@ -59,9 +59,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/receiving/view.mako b/tailbone/templates/receiving/view.mako index 45a8d66b..710dec4a 100644 --- a/tailbone/templates/receiving/view.mako +++ b/tailbone/templates/receiving/view.mako @@ -139,9 +139,15 @@ % endif -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="object_helpers()"> + ${self.render_status_breakdown()} + ${self.render_po_vs_invoice_helper()} + ${self.render_execute_helper()} + ${self.render_tools_helper()} + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost: - - -${parent.body()} diff --git a/tailbone/templates/receiving/view_row.mako b/tailbone/templates/receiving/view_row.mako index 5077539c..086754c6 100644 --- a/tailbone/templates/receiving/view_row.mako +++ b/tailbone/templates/receiving/view_row.mako @@ -484,9 +484,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/choose.mako b/tailbone/templates/reports/generated/choose.mako index a952fb6a..0921530c 100644 --- a/tailbone/templates/reports/generated/choose.mako +++ b/tailbone/templates/reports/generated/choose.mako @@ -53,13 +53,13 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/delete.mako b/tailbone/templates/reports/generated/delete.mako index bce54662..f60a9819 100644 --- a/tailbone/templates/reports/generated/delete.mako +++ b/tailbone/templates/reports/generated/delete.mako @@ -1,16 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/delete.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/view.mako b/tailbone/templates/reports/generated/view.mako index e5bcc9e4..cce6f346 100644 --- a/tailbone/templates/reports/generated/view.mako +++ b/tailbone/templates/reports/generated/view.mako @@ -23,16 +23,11 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/inventory.mako b/tailbone/templates/reports/inventory.mako index f051959f..cc5adc10 100644 --- a/tailbone/templates/reports/inventory.mako +++ b/tailbone/templates/reports/inventory.mako @@ -48,15 +48,10 @@ ${h.end_form()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/ordering.mako b/tailbone/templates/reports/ordering.mako index 1e526792..61ccdb16 100644 --- a/tailbone/templates/reports/ordering.mako +++ b/tailbone/templates/reports/ordering.mako @@ -81,9 +81,9 @@ <%def name="extra_fields()"> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako index 1d5cb14f..00ac1503 100644 --- a/tailbone/templates/reports/problems/view.mako +++ b/tailbone/templates/reports/problems/view.mako @@ -62,9 +62,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/roles/create.mako b/tailbone/templates/roles/create.mako index 625b2675..89dd56c3 100644 --- a/tailbone/templates/roles/create.mako +++ b/tailbone/templates/roles/create.mako @@ -6,15 +6,11 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/roles/edit.mako b/tailbone/templates/roles/edit.mako index 67f63013..e77cca33 100644 --- a/tailbone/templates/roles/edit.mako +++ b/tailbone/templates/roles/edit.mako @@ -6,15 +6,11 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/roles/view.mako b/tailbone/templates/roles/view.mako index 0dc2956f..f5588695 100644 --- a/tailbone/templates/roles/view.mako +++ b/tailbone/templates/roles/view.mako @@ -6,9 +6,9 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/settings/email/configure.mako b/tailbone/templates/settings/email/configure.mako index ef487809..f9c815c2 100644 --- a/tailbone/templates/settings/email/configure.mako +++ b/tailbone/templates/settings/email/configure.mako @@ -86,9 +86,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/settings/email/index.mako b/tailbone/templates/settings/email/index.mako index 050a5833..ab8d6fa4 100644 --- a/tailbone/templates/settings/email/index.mako +++ b/tailbone/templates/settings/email/index.mako @@ -15,10 +15,10 @@ ${parent.render_grid_component()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('configure'): - % endif - -${parent.body()} diff --git a/tailbone/templates/settings/email/view.mako b/tailbone/templates/settings/email/view.mako index c1bc5ed4..73ad7066 100644 --- a/tailbone/templates/settings/email/view.mako +++ b/tailbone/templates/settings/email/view.mako @@ -6,8 +6,8 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + diff --git a/tailbone/templates/tables/create.mako b/tailbone/templates/tables/create.mako index 4fc2eb96..34844c5c 100644 --- a/tailbone/templates/tables/create.mako +++ b/tailbone/templates/tables/create.mako @@ -695,9 +695,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/tempmon/appliances/view.mako b/tailbone/templates/tempmon/appliances/view.mako index 7dd9314a..a55af922 100644 --- a/tailbone/templates/tempmon/appliances/view.mako +++ b/tailbone/templates/tempmon/appliances/view.mako @@ -8,14 +8,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/tempmon/clients/view.mako b/tailbone/templates/tempmon/clients/view.mako index b1db423b..434da4c8 100644 --- a/tailbone/templates/tempmon/clients/view.mako +++ b/tailbone/templates/tempmon/clients/view.mako @@ -22,14 +22,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/tempmon/dashboard.mako b/tailbone/templates/tempmon/dashboard.mako index 396b0e68..befaf8b4 100644 --- a/tailbone/templates/tempmon/dashboard.mako +++ b/tailbone/templates/tempmon/dashboard.mako @@ -59,9 +59,9 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/tempmon/probes/graph.mako b/tailbone/templates/tempmon/probes/graph.mako index 412f25dd..94a440e0 100644 --- a/tailbone/templates/tempmon/probes/graph.mako +++ b/tailbone/templates/tempmon/probes/graph.mako @@ -66,9 +66,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/themes/butterball/base.mako b/tailbone/templates/themes/butterball/base.mako index 306b3430..14616474 100644 --- a/tailbone/templates/themes/butterball/base.mako +++ b/tailbone/templates/themes/butterball/base.mako @@ -20,38 +20,21 @@ -
    +
    ## TODO: this must come before the self.body() call..but why? ${declare_formposter_mixin()} - ## global components used by various (but not all) pages - ${make_field_components()} - ${make_grid_filter_components()} - - ## global components for buefy-based template compatibility - ${make_http_plugin()} - ${make_buefy_plugin()} - ${make_buefy_components()} - - ## special global components, used by WholePage - ${self.make_menu_search_component()} - ${page_help.render_template()} - ${page_help.declare_vars()} - % if request.has_perm('common.feedback'): - ${self.make_feedback_component()} - % endif - - ## WholePage component - ${self.make_whole_page_component()} - ## content body from derived/child template ${self.body()} ## Vue app - ${self.make_whole_page_app()} + ${self.render_vue_templates()} + ${self.modify_vue_vars()} + ${self.make_vue_components()} + ${self.make_vue_app()} @@ -596,7 +579,7 @@ -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> - -## ${multi_file_upload.render_template()} <%def name="render_this_page_component()"> @@ -1068,9 +1049,7 @@ % endif -<%def name="declare_whole_page_vars()"> -## ${multi_file_upload.declare_vars()} - +<%def name="render_vue_script_whole_page()"> -<%def name="modify_whole_page_vars()"> +############################## +## vue components + app +############################## -## TODO: do we really need this? -## <%def name="finalize_whole_page_vars()"> +<%def name="render_vue_templates()"> +## ${multi_file_upload.render_template()} +## ${multi_file_upload.declare_vars()} -<%def name="make_whole_page_component()"> + ## global components used by various (but not all) pages + ${make_field_components()} + ${make_grid_filter_components()} + + ## global components for buefy-based template compatibility + ${make_http_plugin()} + ${make_buefy_plugin()} + ${make_buefy_components()} + + ## special global components, used by WholePage + ${self.make_menu_search_component()} + ${page_help.render_template()} + ${page_help.declare_vars()} + % if request.has_perm('common.feedback'): + ${self.make_feedback_component()} + % endif + + ## DEPRECATED; called for back-compat ${self.render_whole_page_template()} + + ## DEPRECATED; called for back-compat ${self.declare_whole_page_vars()} + + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.render_vue_script_whole_page()} + + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat ${self.modify_whole_page_vars()} -## ${self.finalize_whole_page_vars()} + +<%def name="make_vue_components()"> ${page_help.make_component()} -## ${multi_file_upload.make_component()} + ## ${multi_file_upload.make_component()} + ## DEPRECATED; called for back-compat (?) + ${self.make_whole_page_component()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> <% request.register_component('whole-page', 'WholePage') %> +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} + + +## DEPRECATED; remains for back-compat <%def name="make_whole_page_app()"> + +############################## +## DEPRECATED +############################## + +<%def name="declare_whole_page_vars()"> + +<%def name="modify_whole_page_vars()"> diff --git a/tailbone/templates/trainwreck/transactions/configure.mako b/tailbone/templates/trainwreck/transactions/configure.mako index 4569759b..10c57e18 100644 --- a/tailbone/templates/trainwreck/transactions/configure.mako +++ b/tailbone/templates/trainwreck/transactions/configure.mako @@ -62,14 +62,9 @@
    -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/rollover.mako b/tailbone/templates/trainwreck/transactions/rollover.mako index b36e7bc3..f26515b5 100644 --- a/tailbone/templates/trainwreck/transactions/rollover.mako +++ b/tailbone/templates/trainwreck/transactions/rollover.mako @@ -48,14 +48,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/view.mako b/tailbone/templates/trainwreck/transactions/view.mako index 02950941..630950cf 100644 --- a/tailbone/templates/trainwreck/transactions/view.mako +++ b/tailbone/templates/trainwreck/transactions/view.mako @@ -1,15 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/view_row.mako b/tailbone/templates/trainwreck/transactions/view_row.mako index 9c76f7bd..2507492e 100644 --- a/tailbone/templates/trainwreck/transactions/view_row.mako +++ b/tailbone/templates/trainwreck/transactions/view_row.mako @@ -1,16 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view_row.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/units-of-measure/index.mako b/tailbone/templates/units-of-measure/index.mako index 597cabfd..4815fc79 100644 --- a/tailbone/templates/units-of-measure/index.mako +++ b/tailbone/templates/units-of-measure/index.mako @@ -51,20 +51,17 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('collect_wild_uoms'): - + % endif - - -${parent.body()} diff --git a/tailbone/templates/upgrades/configure.mako b/tailbone/templates/upgrades/configure.mako index f7af685c..9439f830 100644 --- a/tailbone/templates/upgrades/configure.mako +++ b/tailbone/templates/upgrades/configure.mako @@ -111,9 +111,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/upgrades/view.mako b/tailbone/templates/upgrades/view.mako index 6ae110e0..c3fca81d 100644 --- a/tailbone/templates/upgrades/view.mako +++ b/tailbone/templates/upgrades/view.mako @@ -137,11 +137,11 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/users/preferences.mako b/tailbone/templates/users/preferences.mako index c2e17396..ecfdd1c7 100644 --- a/tailbone/templates/users/preferences.mako +++ b/tailbone/templates/users/preferences.mako @@ -42,14 +42,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako index 06087927..d1afd218 100644 --- a/tailbone/templates/users/view.mako +++ b/tailbone/templates/users/view.mako @@ -76,10 +76,10 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('manage_api_tokens'): - % endif - - -${parent.body()} diff --git a/tailbone/templates/vendors/configure.mako b/tailbone/templates/vendors/configure.mako index 79dad455..6b135346 100644 --- a/tailbone/templates/vendors/configure.mako +++ b/tailbone/templates/vendors/configure.mako @@ -44,14 +44,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/views/model/create.mako b/tailbone/templates/views/model/create.mako index c5e22cfb..e902fd48 100644 --- a/tailbone/templates/views/model/create.mako +++ b/tailbone/templates/views/model/create.mako @@ -259,9 +259,9 @@ def includeme(config): -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} diff --git a/tailbone/templates/workorders/view.mako b/tailbone/templates/workorders/view.mako index 8740b4c9..432e011d 100644 --- a/tailbone/templates/workorders/view.mako +++ b/tailbone/templates/workorders/view.mako @@ -145,9 +145,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} From 59bd58aca768f9e18a1e3db7447a576c48d29191 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 20 Aug 2024 13:46:40 -0500 Subject: [PATCH 022/102] feat: add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy hoping to eventually replace the 'default' view with this one, if all goes well. definitely needs more testing and is not exposed as an option yet, unless configured --- tailbone/app.py | 3 +- tailbone/forms/core.py | 15 +- tailbone/grids/core.py | 14 +- tailbone/static/__init__.py | 5 +- tailbone/templates/appinfo/index.mako | 4 +- tailbone/templates/base.mako | 2 + tailbone/templates/batch/index.mako | 9 +- tailbone/templates/batch/view.mako | 20 +- tailbone/templates/form.mako | 5 +- tailbone/templates/themes/waterpark/base.mako | 486 ++++++++++++++++++ .../templates/themes/waterpark/configure.mako | 2 + tailbone/templates/themes/waterpark/form.mako | 2 + .../themes/waterpark/master/configure.mako | 2 + .../themes/waterpark/master/create.mako | 2 + .../themes/waterpark/master/delete.mako | 46 ++ .../themes/waterpark/master/edit.mako | 2 + .../themes/waterpark/master/form.mako | 2 + .../themes/waterpark/master/index.mako | 294 +++++++++++ .../themes/waterpark/master/view.mako | 2 + tailbone/templates/themes/waterpark/page.mako | 48 ++ tailbone/views/master.py | 12 +- tailbone/views/people.py | 2 +- tests/util.py | 2 +- 23 files changed, 937 insertions(+), 44 deletions(-) create mode 100644 tailbone/templates/themes/waterpark/base.mako create mode 100644 tailbone/templates/themes/waterpark/configure.mako create mode 100644 tailbone/templates/themes/waterpark/form.mako create mode 100644 tailbone/templates/themes/waterpark/master/configure.mako create mode 100644 tailbone/templates/themes/waterpark/master/create.mako create mode 100644 tailbone/templates/themes/waterpark/master/delete.mako create mode 100644 tailbone/templates/themes/waterpark/master/edit.mako create mode 100644 tailbone/templates/themes/waterpark/master/form.mako create mode 100644 tailbone/templates/themes/waterpark/master/index.mako create mode 100644 tailbone/templates/themes/waterpark/master/view.mako create mode 100644 tailbone/templates/themes/waterpark/page.mako diff --git a/tailbone/app.py b/tailbone/app.py index ad9663cf..b7262866 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -321,7 +321,8 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - settings.setdefault('mako.directories', ['tailbone:templates']) + settings.setdefault('mako.directories', ['tailbone:templates', + 'wuttaweb:templates']) rattail_config = make_rattail_config(settings) pyramid_config = make_pyramid_config(settings) pyramid_config.include('tailbone') diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 2f1c9370..059b212a 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -905,7 +905,8 @@ class Form(object): def render_vue_template(self, template='/forms/deform.mako', **context): """ """ - return self.render_deform(template=template, **context) + output = self.render_deform(template=template, **context) + return HTML.literal(output) def render_deform(self, dform=None, template=None, **kwargs): if not template: @@ -1220,6 +1221,18 @@ class Form(object): # TODO: again, why does serialize() not return literal? return HTML.literal(field.serialize()) + # TODO: this was copied from wuttaweb; can remove when we align + # Form class structure + def render_vue_finalize(self): + """ """ + 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']) + def render_field_readonly(self, field_name, **kwargs): """ Render the given field completely, but in read-only fashion. diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 6ec55987..eada1041 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -216,39 +216,39 @@ class Grid(WuttaGrid): expose_direct_link=False, **kwargs, ): - if kwargs.get('component'): + if 'component' in kwargs: warnings.warn("component param is deprecated for Grid(); " "please use vue_tagname param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) - if kwargs.get('default_sortkey'): + if 'default_sortkey' in kwargs: warnings.warn("default_sortkey param is deprecated for Grid(); " "please use sort_defaults param instead", DeprecationWarning, stacklevel=2) - if kwargs.get('default_sortdir'): + if 'default_sortdir' in kwargs: warnings.warn("default_sortdir param is deprecated for Grid(); " "please use sort_defaults param instead", DeprecationWarning, stacklevel=2) - if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'): + if 'default_sortkey' in kwargs or 'default_sortdir' in kwargs: sortkey = kwargs.pop('default_sortkey', None) sortdir = kwargs.pop('default_sortdir', 'asc') if sortkey: kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) - if kwargs.get('pageable'): + if 'pageable' in kwargs: warnings.warn("pageable param is deprecated for Grid(); " "please use vue_tagname param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('paginated', kwargs.pop('pageable')) - if kwargs.get('default_pagesize'): + if 'default_pagesize' in kwargs: warnings.warn("default_pagesize param is deprecated for Grid(); " "please use pagesize param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('pagesize', kwargs.pop('default_pagesize')) - if kwargs.get('default_page'): + if 'default_page' in kwargs: warnings.warn("default_page param is deprecated for Grid(); " "please use page param instead", DeprecationWarning, stacklevel=2) diff --git a/tailbone/static/__init__.py b/tailbone/static/__init__.py index 2ad5161a..57700b80 100644 --- a/tailbone/static/__init__.py +++ b/tailbone/static/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,9 +24,8 @@ Static Assets """ -from __future__ import unicode_literals, absolute_import - def includeme(config): + config.include('wuttaweb.static') config.add_static_view('tailbone', 'tailbone:static') config.add_static_view('deform', 'deform:static') diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako index 68244300..75032c1f 100644 --- a/tailbone/templates/appinfo/index.mako +++ b/tailbone/templates/appinfo/index.mako @@ -1,7 +1,7 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/index.mako" /> -<%def name="render_grid_component()"> +<%def name="page_content()">
    @@ -108,7 +108,7 @@
    - ${parent.render_grid_component()} + ${grid.render_vue_tag()}
    diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index a0e58e22..eb950011 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -1,4 +1,5 @@ ## -*- coding: utf-8; -*- +<%namespace file="/wutta-components.mako" import="make_wutta_components" /> <%namespace file="/grids/nav.mako" import="grid_index_nav" /> <%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace name="base_meta" file="/base_meta.mako" /> @@ -955,6 +956,7 @@ <%def name="make_vue_components()"> + ${make_wutta_components()} ${make_grid_filter_components()} ${page_help.make_component()} ${multi_file_upload.make_component()} diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index a1b11b89..bea10a97 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -43,7 +43,7 @@
    - <${execute_form.component} ref="executeResultsForm"> + ${execute_form.render_vue_tag(ref='executeResultsForm')}
    @@ -67,7 +67,7 @@ <%def name="render_vue_templates()"> ${parent.render_vue_templates()} % if master.results_executable and master.has_perm('execute_multiple'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + ${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)} % endif @@ -128,9 +128,6 @@ <%def name="make_vue_components()"> ${parent.make_vue_components()} % if master.results_executable and master.has_perm('execute_multiple'): - + ${execute_form.render_vue_finalize()} % endif diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index cdfa9ba7..7c81ab0e 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -119,8 +119,7 @@
    ${execution_described|n}
    - <${execute_form.component} ref="executeBatchForm"> - + ${execute_form.render_vue_tag(ref='executeBatchForm')}