feat: inherit from wutta base class for Grid

This commit is contained in:
Lance Edgar 2024-08-16 14:34:50 -05:00
parent f7641218cb
commit 2a0b6da2f9
23 changed files with 317 additions and 274 deletions

View file

@ -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):
"""

View file

@ -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)

View file

@ -195,6 +195,7 @@ class POSBatchView(BatchMasterView):
factory = self.get_grid_factory()
g = factory(
self.request,
key=f'{route_prefix}.taxes',
data=[],
columns=[

View file

@ -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'))

View file

@ -385,6 +385,7 @@ class CustomerOrderItemView(MasterView):
factory = self.get_grid_factory()
g = factory(
self.request,
key=f'{route_prefix}.events',
data=[],
columns=[

View file

@ -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"}

View file

@ -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'))

View file

@ -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))

View file

@ -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')

View file

@ -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, []

View file

@ -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

View file

@ -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)'),
],

View file

@ -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))

View file

@ -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)'),
])

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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):

View file

@ -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

View file

@ -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'))

View file

@ -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'))

View file

@ -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

View file

@ -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