From d151758c4851a5a967fa08725b7f3d06e387e74f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 22:52:24 -0500 Subject: [PATCH] feat: add backend pagination support for grids --- src/wuttaweb/grids/base.py | 193 +++++++++++++++++- .../templates/grids/vue_template.mako | 123 ++++++++++- src/wuttaweb/views/base.py | 11 + src/wuttaweb/views/master.py | 23 ++- tests/grids/test_base.py | 122 +++++++++++ tests/views/test_base.py | 50 +++-- tests/views/test_master.py | 8 + 7 files changed, 501 insertions(+), 29 deletions(-) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 5f5b34e..328637d 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -30,9 +30,11 @@ import logging import sqlalchemy as sa +import paginate from pyramid.renderers import render from webhelpers2.html import HTML +from wuttaweb.db import Session from wuttaweb.util import FieldList, get_model_fields, make_json_safe @@ -87,6 +89,9 @@ class Grid: model records) or else an object capable of producing such a list, e.g. SQLAlchemy query. + This is the "full" data set; see also + :meth:`get_visible_data()`. + .. attribute:: labels Dict of column label overrides. @@ -114,9 +119,23 @@ class Grid: .. attribute:: paginated Boolean indicating whether the grid data should be paginated - vs. all data is shown at once. Default is ``False``. + vs. all data shown at once. Default is ``False`` which means + the full set of grid data is sent for each request. - See also :attr:`pagesize` and :attr:`page`. + See also :attr:`pagesize` and :attr:`page`, and + :attr:`paginate_on_backend`. + + .. attribute:: paginate_on_backend + + Boolean indicating whether the grid data should be paginated on + the backend. Default is ``True`` which means only one "page" + of data is sent to the client-side component. + + If this is ``False``, the full set of grid data is sent for + each request, and the client-side Vue component will handle the + pagination. + + Only relevant if :attr:`paginated` is also true. .. attribute:: pagesize_options @@ -157,6 +176,7 @@ class Grid: actions=[], linked_columns=[], paginated=False, + paginate_on_backend=True, pagesize_options=None, pagesize=None, page=1, @@ -177,6 +197,7 @@ class Grid: self.set_columns(columns or self.get_columns()) self.paginated = paginated + self.paginate_on_backend = paginate_on_backend self.pagesize_options = pagesize_options or self.get_pagesize_options() self.pagesize = pagesize or self.get_pagesize() self.page = page @@ -435,6 +456,149 @@ class Grid: return self.pagesize_options[0] + ############################## + # configuration methods + ############################## + + def load_settings(self, store=True): + """ + Load all effective settings for the grid, from the following + places: + + * request params + * user session + + The first value found for a given setting will be applied to + the grid. + + .. note:: + + As of now, "pagination" settings are the only type + supported by this logic. Filter/sort coming soon... + + The overall logic for this method is as follows: + + * collect settings + * apply settings to current grid + * optionally save settings to user session + + Saving the settings to user session will allow the grid to + "remember" its current settings when user refreshes the page. + + :param store: Flag indicating whether the collected settings + should then be saved to the user session. + """ + + # initial default settings + settings = {} + if self.paginated and self.paginate_on_backend: + settings['pagesize'] = self.pagesize + settings['page'] = self.page + + # grab settings from request and/or user session + if self.paginated and self.paginate_on_backend: + self.update_page_settings(settings) + + else: + # no settings were found in request or user session, so + # nothing needs to be saved + store = False + + # maybe store settings in user session, for next time + if store: + self.persist_settings(settings) + + # update ourself to reflect settings + if self.paginated and self.paginate_on_backend: + self.pagesize = settings['pagesize'] + self.page = settings['page'] + + def request_has_settings(self): + """ """ + for key in ['pagesize', 'page']: + if key in self.request.GET: + return True + return False + + def update_page_settings(self, settings): + """ """ + # update the settings dict from request and/or user session + + # pagesize + pagesize = self.request.GET.get('pagesize') + if pagesize is not None: + if pagesize.isdigit(): + settings['pagesize'] = int(pagesize) + else: + pagesize = self.request.session.get(f'grid.{self.key}.pagesize') + if pagesize is not None: + settings['pagesize'] = pagesize + + # page + page = self.request.GET.get('page') + if page is not None: + if page.isdigit(): + settings['page'] = int(page) + else: + page = self.request.session.get(f'grid.{self.key}.page') + if page is not None: + settings['page'] = int(page) + + def persist_settings(self, settings): + """ """ + model = self.app.model + session = Session() + + # func to save a setting value to user session + def persist(key, value=lambda k: settings.get(k)): + skey = f'grid.{self.key}.{key}' + self.request.session[skey] = value(key) + + if self.paginated and self.paginate_on_backend: + persist('pagesize') + persist('page') + + ############################## + # data methods + ############################## + + def get_visible_data(self): + """ + Returns the "effective" visible data for the grid. + + This uses :attr:`data` as the starting point but may morph it + for pagination etc. per the grid settings. + + Code can either access :attr:`data` directly, or call this + method to get only the data for current view (e.g. assuming + pagination is used), depending on the need. + + See also these methods which may be called by this one: + + * :meth:`paginate_data()` + """ + data = self.data or [] + + if self.paginated and self.paginate_on_backend: + self.pager = self.paginate_data(data) + data = self.pager + + return data + + def paginate_data(self, data): + """ + Apply pagination to the given data set, based on grid settings. + + This returns a "pager" object which can then be used as a + "data replacement" in subsequent logic. + + This method is called by :meth:`get_visible_data()`. + """ + pager = paginate.Page(data, + items_per_page=self.pagesize, + page=self.page) + return pager + ############################## # rendering methods ############################## @@ -517,17 +681,18 @@ class Grid: """ Returns a list of Vue-compatible data records. - This uses :attr:`data` as the basis, but may add some extra - values to each record, e.g. URLs for :attr:`actions` etc. + This calls :meth:`get_visible_data()` but then may modify the + result, e.g. to add URLs for :attr:`actions` etc. Importantly, this also ensures each value in the dict is JSON-serializable, using :func:`~wuttaweb.util.make_json_safe()`. :returns: List of data record dicts for use with Vue table - component. + component. May be the full set of data, or just the + current page, per :attr:`paginate_on_backend`. """ - original_data = self.data or [] + original_data = self.get_visible_data() # TODO: at some point i thought it was useful to wrangle the # columns here, but now i can't seem to figure out why..? @@ -578,6 +743,22 @@ class Grid: return data + def get_vue_pager_stats(self): + """ + Returns a simple dict with current grid pager stats. + + This is used when :attr:`paginate_on_backend` is in effect. + """ + pager = self.pager + return { + 'item_count': pager.item_count, + 'items_per_page': pager.items_per_page, + 'page': pager.page, + 'page_count': pager.page_count, + 'first_item': pager.first_item, + 'last_item': pager.last_item, + } + class GridAction: """ diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako index 0505c33..e588450 100644 --- a/src/wuttaweb/templates/grids/vue_template.mako +++ b/src/wuttaweb/templates/grids/vue_template.mako @@ -14,6 +14,11 @@ pagination-size="is-small" :per-page="perPage" :current-page="currentPage" + @page-change="onPageChange" + % if grid.paginate_on_backend: + backend-pagination + :total="pagerStats.item_count" + % endif % endif > @@ -53,8 +58,14 @@
showing + {{ renderNumber(pagerStats.first_item) }} + - {{ renderNumber(pagerStats.last_item) }} + of {{ renderNumber(pagerStats.item_count) }} results;