3
0
Fork 0

fix: add basic "Show Totals" feature for main index grids

subclass must define what to display
This commit is contained in:
Lance Edgar 2026-05-06 22:09:36 -05:00
parent 184aa0630d
commit bbcc17b064
4 changed files with 133 additions and 1 deletions

View file

@ -328,6 +328,7 @@
const ${grid.vue_component} = {
template: '#${grid.vue_tagname}-template',
mixins: [WuttaRequestMixin],
computed: {
recordCount() {
@ -658,8 +659,13 @@
// fetch new data
params.filter = true
this.fetchData(params)
## nb. this is used for "show totals" feature in master/index template
this.appliedFiltersHook()
},
## nb. for now each grid can have "just one" (or no) hook
appliedFiltersHook() {},
getFilterParams() {
const params = {}
for (let filter of this.filters) {

View file

@ -31,6 +31,30 @@
${parent.modify_vue_vars()}
<script>
% if master.has_grid_totals:
${grid.vue_component}Data.gridTotalsHTML = ""
${grid.vue_component}Data.gridTotalsFetching = false
${grid.vue_component}.methods.gridTotalsFetch = function() {
this.gridTotalsFetching = true
const url = "${url(f'{route_prefix}.fetch_grid_totals')}"
this.wuttaGET(url, {}, response => {
this.gridTotalsHTML = response.data.totals_html
this.gridTotalsFetching = false
}, response => {
this.gridTotalsFetching = false
})
}
${grid.vue_component}.methods.appliedFiltersHook = function() {
this.gridTotalsHTML = ""
this.gridTotalsFetching = false
}
% endif
% if master.deletable_bulk and master.has_perm('delete_bulk'):
${grid.vue_component}Data.deleteResultsSubmitting = false

View file

@ -193,6 +193,13 @@ class MasterView(View): # pylint: disable=too-many-public-methods
This is optional; see also :meth:`get_grid_columns()`.
.. attribute:: has_grid_totals
Boolean indicating whether the main grid supports a "Show
Totals" feature; this is false by default.
See also :meth:`fetch_grid_totals()`.
.. attribute:: checkable
Boolean indicating whether the grid should expose per-row
@ -481,6 +488,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
# features
listable = True
has_grid = True
has_grid_totals = False
checkable = False
filterable = True
filter_defaults = None
@ -586,6 +594,42 @@ class MasterView(View): # pylint: disable=too-many-public-methods
return self.render_to_response("index", context)
def fetch_grid_totals(self):
"""
Should return the "totals info" for the main grid, if
applicable. Only relevant when :attr:`has_grid_totals` is
true.
This method is called "on demand" from the client side; totals
are not calculated / shown by default when a grid is first
displayed on the page.
Subclass should override this method to calculate and return
the customized info. Default logic within the template is
expecting a ``totals_html`` key within the dict; this will be
rendered as-is on the page. For instance::
def fetch_grid_totals(self):
from webhelpers2.html import HTML
# get current data set from grid
# nb. this will be filtered and sorted but *not*
# paginated; we want to include all results.
grid = self.make_model_grid(paginated=False)
rows = grid.get_visible_data()
# calculate total
foo_total = sum([row.foo_amount for row in rows])
# render as <span> tag
html = HTML.tag("span", c=f"Foo Total: {foo_total:0.2f}")
return {"totals_html": html}
:returns: Dict of totals info.
"""
return {"totals_html": "TODO: totals go here"}
##############################
# create methods
##############################
@ -2612,7 +2656,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
def make_model_grid(
self, session=None, **kwargs
): # pylint: disable=too-many-branches
): # pylint: disable=too-many-branches,too-many-statements
"""
Create and return a :class:`~wuttaweb.grids.base.Grid`
instance for use with the :meth:`index()` view.
@ -2678,6 +2722,29 @@ class MasterView(View): # pylint: disable=too-many-public-methods
if "tools" not in kwargs:
tools = []
# show totals
if self.has_grid_totals:
button = self.make_button(
"{{ gridTotalsFetching ? 'Working, please wait...' : 'Show Totals' }}",
icon_left="calculator",
**{
"v-if": "!gridTotalsHTML.length",
":disabled": "gridTotalsFetching",
"@click": "gridTotalsFetch()",
},
)
display = HTML.tag(
"div",
class_="control",
style="margin: 0 0.5rem;",
**{
"v-if": "gridTotalsHTML.length",
"v-html": "gridTotalsHTML",
},
)
wrapper = HTML.tag("div", c=[button, display])
tools.append(("show-totals", wrapper))
# delete-bulk
if self.deletable_bulk and self.has_perm("delete_bulk"):
tools.append(("delete-results", self.delete_bulk_make_button()))
@ -4310,6 +4377,21 @@ class MasterView(View): # pylint: disable=too-many-public-methods
f"Browse / search {model_title_plural}",
)
# grid totals
if cls.has_grid_totals:
config.add_route(
f"{route_prefix}.fetch_grid_totals",
f"{url_prefix}/fetch-grid-totals",
request_method="GET",
)
config.add_view(
cls,
attr="fetch_grid_totals",
route_name=f"{route_prefix}.fetch_grid_totals",
permission=f"{permission_prefix}.list",
renderer="json",
)
# create
if cls.creatable:
config.add_route(f"{route_prefix}.create", f"{url_prefix}/new")

View file

@ -34,6 +34,7 @@ class TestMasterView(WebTestCase):
model_key="uuid",
deletable_bulk=True,
has_autocomplete=True,
has_grid_totals=True,
downloadable=True,
executable=True,
configurable=True,
@ -694,6 +695,15 @@ class TestMasterView(WebTestCase):
grid = view.make_model_grid(session=self.session)
self.assertEqual(grid.tools, {})
# "show totals" tool added when applicable
with patch.multiple(
mod.MasterView, model_class=model.Setting, has_grid_totals=True
):
view = self.make_view()
grid = view.make_model_grid(session=self.session)
self.assertEqual(len(grid.tools), 1)
self.assertIn("show-totals", grid.tools)
# delete-results tool added if master/perms allow
with patch.multiple(
mod.MasterView,
@ -763,6 +773,16 @@ class TestMasterView(WebTestCase):
self.assertEqual(len(data), 1)
self.assertIs(data[0], setting)
def test_fetch_grid_totals(self):
view = self.make_view()
# with patch.object(view, "has_grid_totals", new=True):
# nb. default logic just returns generic stub info
totals = view.fetch_grid_totals()
self.assertIsInstance(totals, dict)
self.assertIn("totals_html", totals)
self.assertEqual(totals["totals_html"], "TODO: totals go here")
def test_configure_grid(self):
model = self.app.model