feat: add initial/basic pagination for grids
so far this is only for client-side pagination; which means *all* grid data is dumped to JSON for Vue access. backend pagination coming soon
This commit is contained in:
parent
7feaa844af
commit
dd3d640b1c
|
@ -31,6 +31,7 @@ classifiers = [
|
||||||
requires-python = ">= 3.8"
|
requires-python = ">= 3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ColanderAlchemy",
|
"ColanderAlchemy",
|
||||||
|
"paginate",
|
||||||
"pyramid>=2",
|
"pyramid>=2",
|
||||||
"pyramid_beaker",
|
"pyramid_beaker",
|
||||||
"pyramid_deform",
|
"pyramid_deform",
|
||||||
|
|
|
@ -61,6 +61,11 @@ class Grid:
|
||||||
Presumably unique key for the grid; used to track per-grid
|
Presumably unique key for the grid; used to track per-grid
|
||||||
sort/filter settings etc.
|
sort/filter settings etc.
|
||||||
|
|
||||||
|
.. attribute:: vue_tagname
|
||||||
|
|
||||||
|
String name for Vue component tag. By default this is
|
||||||
|
``'wutta-grid'``. See also :meth:`render_vue_tag()`.
|
||||||
|
|
||||||
.. attribute:: model_class
|
.. attribute:: model_class
|
||||||
|
|
||||||
Model class for the grid, if applicable. When set, this is
|
Model class for the grid, if applicable. When set, this is
|
||||||
|
@ -106,15 +111,43 @@ class Grid:
|
||||||
|
|
||||||
See also :meth:`set_link()` and :meth:`is_linked()`.
|
See also :meth:`set_link()` and :meth:`is_linked()`.
|
||||||
|
|
||||||
.. attribute:: vue_tagname
|
.. attribute:: paginated
|
||||||
|
|
||||||
String name for Vue component tag. By default this is
|
Boolean indicating whether the grid data should be paginated
|
||||||
``'wutta-grid'``. See also :meth:`render_vue_tag()`.
|
vs. all data is shown at once. Default is ``False``.
|
||||||
|
|
||||||
|
See also :attr:`pagesize` and :attr:`page`.
|
||||||
|
|
||||||
|
.. attribute:: pagesize_options
|
||||||
|
|
||||||
|
List of "page size" options for the grid. See also
|
||||||
|
:attr:`pagesize`.
|
||||||
|
|
||||||
|
Only relevant if :attr:`paginated` is true. If not specified,
|
||||||
|
constructor will call :meth:`get_pagesize_options()` to get the
|
||||||
|
value.
|
||||||
|
|
||||||
|
.. attribute:: pagesize
|
||||||
|
|
||||||
|
Number of records to show in a data page. See also
|
||||||
|
:attr:`pagesize_options` and :attr:`page`.
|
||||||
|
|
||||||
|
Only relevant if :attr:`paginated` is true. If not specified,
|
||||||
|
constructor will call :meth:`get_pagesize()` to get the value.
|
||||||
|
|
||||||
|
.. attribute:: page
|
||||||
|
|
||||||
|
The current page number (of data) to display in the grid. See
|
||||||
|
also :attr:`pagesize`.
|
||||||
|
|
||||||
|
Only relevant if :attr:`paginated` is true. If not specified,
|
||||||
|
constructor will assume ``1`` (first page).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request,
|
request,
|
||||||
|
vue_tagname='wutta-grid',
|
||||||
model_class=None,
|
model_class=None,
|
||||||
key=None,
|
key=None,
|
||||||
columns=None,
|
columns=None,
|
||||||
|
@ -123,9 +156,13 @@ class Grid:
|
||||||
renderers={},
|
renderers={},
|
||||||
actions=[],
|
actions=[],
|
||||||
linked_columns=[],
|
linked_columns=[],
|
||||||
vue_tagname='wutta-grid',
|
paginated=False,
|
||||||
|
pagesize_options=None,
|
||||||
|
pagesize=None,
|
||||||
|
page=1,
|
||||||
):
|
):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
self.vue_tagname = vue_tagname
|
||||||
self.model_class = model_class
|
self.model_class = model_class
|
||||||
self.key = key
|
self.key = key
|
||||||
self.data = data
|
self.data = data
|
||||||
|
@ -133,13 +170,17 @@ class Grid:
|
||||||
self.renderers = renderers or {}
|
self.renderers = renderers or {}
|
||||||
self.actions = actions or []
|
self.actions = actions or []
|
||||||
self.linked_columns = linked_columns or []
|
self.linked_columns = linked_columns or []
|
||||||
self.vue_tagname = vue_tagname
|
|
||||||
|
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
self.set_columns(columns or self.get_columns())
|
self.set_columns(columns or self.get_columns())
|
||||||
|
|
||||||
|
self.paginated = paginated
|
||||||
|
self.pagesize_options = pagesize_options or self.get_pagesize_options()
|
||||||
|
self.pagesize = pagesize or self.get_pagesize()
|
||||||
|
self.page = page
|
||||||
|
|
||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
"""
|
"""
|
||||||
Returns the official list of column names for the grid, or
|
Returns the official list of column names for the grid, or
|
||||||
|
@ -340,6 +381,64 @@ class Grid:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# paging methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def get_pagesize_options(self, default=None):
|
||||||
|
"""
|
||||||
|
Returns a list of default page size options for the grid.
|
||||||
|
|
||||||
|
It will check config but if no setting exists, will fall
|
||||||
|
back to::
|
||||||
|
|
||||||
|
[5, 10, 20, 50, 100, 200]
|
||||||
|
|
||||||
|
:param default: Alternate default value to return if none is
|
||||||
|
configured.
|
||||||
|
|
||||||
|
This method is intended for use in the constructor. Code can
|
||||||
|
instead access :attr:`pagesize_options` directly.
|
||||||
|
"""
|
||||||
|
options = self.config.get_list('wuttaweb.grids.default_pagesize_options')
|
||||||
|
if options:
|
||||||
|
options = [int(size) for size in options
|
||||||
|
if size.isdigit()]
|
||||||
|
if options:
|
||||||
|
return options
|
||||||
|
|
||||||
|
return default or [5, 10, 20, 50, 100, 200]
|
||||||
|
|
||||||
|
def get_pagesize(self, default=None):
|
||||||
|
"""
|
||||||
|
Returns the default page size for the grid.
|
||||||
|
|
||||||
|
It will check config but if no setting exists, will fall back
|
||||||
|
to a value from :attr:`pagesize_options` (will return ``20`` if
|
||||||
|
that is listed; otherwise the "first" option).
|
||||||
|
|
||||||
|
:param default: Alternate default value to return if none is
|
||||||
|
configured.
|
||||||
|
|
||||||
|
This method is intended for use in the constructor. Code can
|
||||||
|
instead access :attr:`pagesize` directly.
|
||||||
|
"""
|
||||||
|
size = self.config.get_int('wuttaweb.grids.default_pagesize')
|
||||||
|
if size:
|
||||||
|
return size
|
||||||
|
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if 20 in self.pagesize_options:
|
||||||
|
return 20
|
||||||
|
|
||||||
|
return self.pagesize_options[0]
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# rendering methods
|
||||||
|
##############################
|
||||||
|
|
||||||
def render_vue_tag(self, **kwargs):
|
def render_vue_tag(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Render the Vue component tag for the grid.
|
Render the Vue component tag for the grid.
|
||||||
|
|
|
@ -2,8 +2,20 @@
|
||||||
|
|
||||||
<script type="text/x-template" id="${grid.vue_tagname}-template">
|
<script type="text/x-template" id="${grid.vue_tagname}-template">
|
||||||
<${b}-table :data="data"
|
<${b}-table :data="data"
|
||||||
|
:loading="loading"
|
||||||
|
|
||||||
|
narrowed
|
||||||
hoverable
|
hoverable
|
||||||
:loading="loading">
|
icon-pack="fas"
|
||||||
|
|
||||||
|
## paging
|
||||||
|
% if grid.paginated:
|
||||||
|
paginated
|
||||||
|
pagination-size="is-small"
|
||||||
|
:per-page="perPage"
|
||||||
|
:current-page="currentPage"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
|
|
||||||
% for column in grid.get_vue_columns():
|
% for column in grid.get_vue_columns():
|
||||||
<${b}-table-column field="${column['field']}"
|
<${b}-table-column field="${column['field']}"
|
||||||
|
@ -34,21 +46,51 @@
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
% if grid.paginated:
|
||||||
|
<template #footer>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<div></div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
|
<span>
|
||||||
|
showing
|
||||||
|
</span>
|
||||||
|
<b-select v-model="perPage"
|
||||||
|
size="is-small">
|
||||||
|
<option v-for="size in pageSizeOptions"
|
||||||
|
:value="size">
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
<span>
|
||||||
|
per page
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
% endif
|
||||||
|
|
||||||
</${b}-table>
|
</${b}-table>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
let ${grid.vue_component} = {
|
const ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
||||||
|
|
||||||
|
const ${grid.vue_component}Data = {
|
||||||
|
data: ${grid.vue_component}CurrentData,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
## paging
|
||||||
|
% if grid.paginated:
|
||||||
|
pageSizeOptions: ${json.dumps(grid.pagesize_options)|n},
|
||||||
|
perPage: ${json.dumps(grid.pagesize)|n},
|
||||||
|
currentPage: ${json.dumps(grid.page)|n},
|
||||||
|
% endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const ${grid.vue_component} = {
|
||||||
template: '#${grid.vue_tagname}-template',
|
template: '#${grid.vue_tagname}-template',
|
||||||
methods: {},
|
methods: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
|
||||||
|
|
||||||
let ${grid.vue_component}Data = {
|
|
||||||
data: ${grid.vue_component}CurrentData,
|
|
||||||
loading: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -181,6 +181,14 @@ class MasterView(View):
|
||||||
|
|
||||||
This is optional; see also :meth:`get_grid_columns()`.
|
This is optional; see also :meth:`get_grid_columns()`.
|
||||||
|
|
||||||
|
.. attribute:: paginated
|
||||||
|
|
||||||
|
Boolean indicating whether the grid data for the
|
||||||
|
:meth:`index()` view should be paginated. Default is ``True``.
|
||||||
|
|
||||||
|
This is used by :meth:`make_model_grid()` to set the grid's
|
||||||
|
:attr:`~wuttaweb.grids.base.Grid.paginated` flag.
|
||||||
|
|
||||||
.. attribute:: creatable
|
.. attribute:: creatable
|
||||||
|
|
||||||
Boolean indicating whether the view model supports "creating" -
|
Boolean indicating whether the view model supports "creating" -
|
||||||
|
@ -229,6 +237,7 @@ class MasterView(View):
|
||||||
# features
|
# features
|
||||||
listable = True
|
listable = True
|
||||||
has_grid = True
|
has_grid = True
|
||||||
|
paginated = True
|
||||||
creatable = True
|
creatable = True
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
|
@ -1061,6 +1070,8 @@ class MasterView(View):
|
||||||
|
|
||||||
kwargs['actions'] = actions
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
|
kwargs.setdefault('paginated', self.paginated)
|
||||||
|
|
||||||
grid = self.make_grid(**kwargs)
|
grid = self.make_grid(**kwargs)
|
||||||
self.configure_grid(grid)
|
self.configure_grid(grid)
|
||||||
return grid
|
return grid
|
||||||
|
|
|
@ -8,24 +8,10 @@ from pyramid import testing
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb.grids import base
|
from wuttaweb.grids import base
|
||||||
from wuttaweb.forms import FieldList
|
from wuttaweb.forms import FieldList
|
||||||
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestGrid(TestCase):
|
class TestGrid(WebTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.config = WuttaConfig(defaults={
|
|
||||||
'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler',
|
|
||||||
})
|
|
||||||
self.app = self.config.get_app()
|
|
||||||
|
|
||||||
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
|
|
||||||
|
|
||||||
self.pyramid_config = testing.setUp(request=self.request, settings={
|
|
||||||
'mako.directories': ['wuttaweb:templates'],
|
|
||||||
})
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
testing.tearDown()
|
|
||||||
|
|
||||||
def make_grid(self, request=None, **kwargs):
|
def make_grid(self, request=None, **kwargs):
|
||||||
return base.Grid(request or self.request, **kwargs)
|
return base.Grid(request or self.request, **kwargs)
|
||||||
|
@ -144,6 +130,44 @@ class TestGrid(TestCase):
|
||||||
self.assertFalse(grid.is_linked('foo'))
|
self.assertFalse(grid.is_linked('foo'))
|
||||||
self.assertTrue(grid.is_linked('bar'))
|
self.assertTrue(grid.is_linked('bar'))
|
||||||
|
|
||||||
|
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 config
|
||||||
|
self.config.setdefault('wuttaweb.grids.default_pagesize_options', '1 2 3')
|
||||||
|
options = grid.get_pagesize_options()
|
||||||
|
self.assertEqual(options, [1, 2, 3])
|
||||||
|
|
||||||
|
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 config
|
||||||
|
self.config.setdefault('wuttaweb.grids.default_pagesize', '15')
|
||||||
|
size = grid.get_pagesize()
|
||||||
|
self.assertEqual(size, 15)
|
||||||
|
|
||||||
def test_render_vue_tag(self):
|
def test_render_vue_tag(self):
|
||||||
grid = self.make_grid(columns=['foo', 'bar'])
|
grid = self.make_grid(columns=['foo', 'bar'])
|
||||||
html = grid.render_vue_tag()
|
html = grid.render_vue_tag()
|
||||||
|
|
Loading…
Reference in a new issue