From 2a0b6da2f9169c22c099ca2c367a3ab2d89fa6e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 14:34:50 -0500 Subject: [PATCH] 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