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.
This commit is contained in:
Lance Edgar 2024-08-16 18:45:04 -05:00
parent 2a0b6da2f9
commit 9da2a148c6
5 changed files with 195 additions and 47 deletions

View file

@ -31,6 +31,7 @@ import logging
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from wuttjamaican.util import UNSPECIFIED
from rattail.db.types import GPCType from rattail.db.types import GPCType
from rattail.util import prettify, pretty_boolean from rattail.util import prettify, pretty_boolean
@ -209,9 +210,6 @@ class Grid(WuttaGrid):
sorters={}, sorters={},
default_sortkey=None, default_sortkey=None,
default_sortdir='asc', default_sortdir='asc',
pageable=False,
default_pagesize=None,
default_page=1,
checkboxes=False, checkboxes=False,
checked=None, checked=None,
check_handler=None, check_handler=None,
@ -233,7 +231,26 @@ class Grid(WuttaGrid):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs.setdefault('vue_tagname', kwargs.pop('component')) 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.setdefault('vue_tagname', 'tailbone-grid')
kwargs['key'] = key kwargs['key'] = key
@ -272,10 +289,6 @@ class Grid(WuttaGrid):
self.default_sortkey = default_sortkey self.default_sortkey = default_sortkey
self.default_sortdir = default_sortdir self.default_sortdir = default_sortdir
self.pageable = pageable
self.default_pagesize = default_pagesize
self.default_page = default_page
self.checkboxes = checkboxes self.checkboxes = checkboxes
self.checked = checked self.checked = checked
if self.checked is None: if self.checked is None:
@ -333,6 +346,16 @@ class Grid(WuttaGrid):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
return self.vue_component 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): def hide_column(self, key):
""" """
This *removes* a column from the grid, altogether. This *removes* a column from the grid, altogether.
@ -756,18 +779,61 @@ class Grid(WuttaGrid):
keyfunc = lambda v: v[key] keyfunc = lambda v: v[key]
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc') 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: if self.default_pagesize:
return self.default_pagesize return self.default_pagesize
pagesize = self.request.rattail_config.getint('tailbone', return self.get_pagesize()
'grid.default_pagesize',
default=0)
if pagesize:
return pagesize
options = self.get_pagesize_options()
return options[0]
def load_settings(self, store=True): def load_settings(self, store=True):
""" """
@ -789,9 +855,9 @@ class Grid(WuttaGrid):
settings['sorters.1.dir'] = self.default_sortdir settings['sorters.1.dir'] = self.default_sortdir
else: else:
settings['sorters.length'] = 0 settings['sorters.length'] = 0
if self.pageable: if self.paginated:
settings['pagesize'] = self.get_default_pagesize() settings['pagesize'] = self.pagesize
settings['page'] = self.default_page settings['page'] = self.page
if self.filterable: if self.filterable:
for filtr in self.iter_filters(): for filtr in self.iter_filters():
settings['filter.{}.active'.format(filtr.key)] = filtr.default_active settings['filter.{}.active'.format(filtr.key)] = filtr.default_active
@ -867,7 +933,7 @@ class Grid(WuttaGrid):
'field': settings[f'sorters.{i}.key'], 'field': settings[f'sorters.{i}.key'],
'order': settings[f'sorters.{i}.dir'], 'order': settings[f'sorters.{i}.dir'],
}) })
if self.pageable: if self.paginated:
self.pagesize = settings['pagesize'] self.pagesize = settings['pagesize']
self.page = settings['page'] self.page = settings['page']
@ -971,7 +1037,7 @@ class Grid(WuttaGrid):
merge(f'sorters.{i}.key') merge(f'sorters.{i}.key')
merge(f'sorters.{i}.dir') merge(f'sorters.{i}.dir')
if self.pageable: if self.paginated:
merge('pagesize', int) merge('pagesize', int)
merge('page', int) merge('page', int)
@ -1154,7 +1220,7 @@ class Grid(WuttaGrid):
:param settings: Dictionary of initial settings, which is to be updated. :param settings: Dictionary of initial settings, which is to be updated.
""" """
if not self.pageable: if not self.paginated:
return return
pagesize = self.request.GET.get('pagesize') pagesize = self.request.GET.get('pagesize')
@ -1231,7 +1297,7 @@ class Grid(WuttaGrid):
persist(f'sorters.{i}.key') persist(f'sorters.{i}.key')
persist(f'sorters.{i}.dir') persist(f'sorters.{i}.dir')
if self.pageable: if self.paginated:
persist('pagesize') persist('pagesize')
persist('page') persist('page')
@ -1355,7 +1421,7 @@ class Grid(WuttaGrid):
data = self.filter_data(data) data = self.filter_data(data)
if self.sortable: if self.sortable:
data = self.sort_data(data) data = self.sort_data(data)
if self.pageable: if self.paginated:
self.pager = self.paginate_data(data) self.pager = self.paginate_data(data)
data = self.pager data = self.pager
return data return data
@ -1580,18 +1646,6 @@ class Grid(WuttaGrid):
return tags.checkbox('checkbox-{}-{}'.format(self.key, self.get_row_key(item)), return tags.checkbox('checkbox-{}-{}'.format(self.key, self.get_row_key(item)),
checked=self.checked(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): def has_static_data(self):
""" """
Should return ``True`` if the grid data can be considered "static" Should return ``True`` if the grid data can be considered "static"
@ -1734,7 +1788,7 @@ class Grid(WuttaGrid):
results['checked_rows_code'] = '[{}]'.format( results['checked_rows_code'] = '[{}]'.format(
', '.join(['{}[{}]'.format(var, i) for i in checked])) ', '.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['total_items'] = self.pager.item_count
results['per_page'] = self.pager.items_per_page results['per_page'] = self.pager.items_per_page
results['page'] = self.pager.page results['page'] = self.pager.page

View file

@ -107,12 +107,14 @@
@cellclick="cellClick" @cellclick="cellClick"
% endif % endif
% if grid.paginated:
:paginated="paginated" :paginated="paginated"
:per-page="perPage" :per-page="perPage"
:current-page="currentPage" :current-page="currentPage"
backend-pagination backend-pagination
:total="total" :total="total"
@page-change="onPageChange" @page-change="onPageChange"
% endif
## TODO: should let grid (or master view) decide how to set these? ## TODO: should let grid (or master view) decide how to set these?
icon-pack="fas" icon-pack="fas"
@ -203,7 +205,7 @@
<div></div> <div></div>
% endif % endif
% if getattr(grid, 'pageable', False): % if grid.paginated:
<div v-if="firstItem" <div v-if="firstItem"
style="display: flex; gap: 0.5rem; align-items: center;"> style="display: flex; gap: 0.5rem; align-items: center;">
<span> <span>
@ -255,12 +257,14 @@
checkedRows: ${grid_data['checked_rows_code']|n}, checkedRows: ${grid_data['checked_rows_code']|n},
% endif % 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)}, 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}, perPage: ${json.dumps(grid.pagesize if grid.paginated else None)|n},
currentPage: ${json.dumps(grid.page if getattr(grid, 'pageable', False) else None)|n}, currentPage: ${json.dumps(grid.page if grid.paginated else None)|n},
firstItem: ${json.dumps(grid_data['first_item'] if getattr(grid, 'pageable', False) else None)|n}, firstItem: ${json.dumps(grid_data['first_item'] if grid.paginated else None)|n},
lastItem: ${json.dumps(grid_data['last_item'] if getattr(grid, 'pageable', False) else None)|n}, lastItem: ${json.dumps(grid_data['last_item'] if grid.paginated else None)|n},
% endif
% if getattr(grid, 'sortable', False): % if getattr(grid, 'sortable', False):
@ -439,7 +443,7 @@
params['sort'+i+'dir'] = this.backendSorters[i-1].order params['sort'+i+'dir'] = this.backendSorters[i-1].order
} }
% endif % endif
% if getattr(grid, 'pageable', False): % if grid.paginated:
params.pagesize = this.perPage params.pagesize = this.perPage
params.page = this.currentPage params.page = this.currentPage
% endif % endif

View file

@ -439,7 +439,7 @@ class MasterView(View):
'filterable': self.filterable, 'filterable': self.filterable,
'use_byte_string_filters': self.use_byte_string_filters, 'use_byte_string_filters': self.use_byte_string_filters,
'sortable': self.sortable, 'sortable': self.sortable,
'pageable': self.pageable, 'paginated': self.pageable,
'extra_row_class': self.grid_extra_class, 'extra_row_class': self.grid_extra_class,
'url': lambda obj: self.get_action_url('view', obj), 'url': lambda obj: self.get_action_url('view', obj),
'checkboxes': checkboxes, 'checkboxes': checkboxes,
@ -589,7 +589,7 @@ class MasterView(View):
} }
if self.rows_default_pagesize: 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: if self.has_rows and 'actions' not in defaults:
actions = [] actions = []

View file

@ -45,6 +45,10 @@ class PersonView(wutta.PersonView):
model_class = Person model_class = Person
Session = Session Session = Session
# TODO: /grids/complete.mako is too aggressive for the
# limited support we have in wuttaweb thus far
paginated = False
labels = { labels = {
'display_name': "Full Name", 'display_name': "Full Name",
} }

View file

@ -19,6 +19,32 @@ class TestGrid(WebTestCase):
grid = self.make_grid('foo') grid = self.make_grid('foo')
self.assertIsInstance(grid, mod.Grid) 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): def test_vue_tagname(self):
# default # default
@ -133,6 +159,66 @@ class TestGrid(WebTestCase):
grid.set_action_urls(setting, setting, 0) grid.set_action_urls(setting, setting, 0)
self.assertEqual(setting['_action_url_view'], '/blarg') 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): def test_render_vue_tag(self):
model = self.app.model model = self.app.model