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