diff --git a/CHANGELOG.md b/CHANGELOG.md
index dad38d5..2e22370 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,6 @@ All notable changes to wuttaweb will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## v0.9.0 (2024-08-16)
-
-### Feat
-
-- add backend pagination support for grids
-- add initial/basic pagination for grids
-
## v0.8.1 (2024-08-15)
### Fix
diff --git a/pyproject.toml b/pyproject.toml
index 9e092c6..e07c98b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.9.0"
+version = "0.8.1"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@@ -31,7 +31,6 @@ classifiers = [
requires-python = ">= 3.8"
dependencies = [
"ColanderAlchemy",
- "paginate",
"pyramid>=2",
"pyramid_beaker",
"pyramid_deform",
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index 328637d..740607c 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -30,11 +30,9 @@ 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
@@ -63,11 +61,6 @@ class Grid:
Presumably unique key for the grid; used to track per-grid
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
Model class for the grid, if applicable. When set, this is
@@ -89,9 +82,6 @@ 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.
@@ -116,57 +106,15 @@ class Grid:
See also :meth:`set_link()` and :meth:`is_linked()`.
- .. attribute:: paginated
+ .. attribute:: vue_tagname
- Boolean indicating whether the grid data should be paginated
- 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`, 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
-
- 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).
+ String name for Vue component tag. By default this is
+ ``'wutta-grid'``. See also :meth:`render_vue_tag()`.
"""
def __init__(
self,
request,
- vue_tagname='wutta-grid',
model_class=None,
key=None,
columns=None,
@@ -175,14 +123,9 @@ class Grid:
renderers={},
actions=[],
linked_columns=[],
- paginated=False,
- paginate_on_backend=True,
- pagesize_options=None,
- pagesize=None,
- page=1,
+ vue_tagname='wutta-grid',
):
self.request = request
- self.vue_tagname = vue_tagname
self.model_class = model_class
self.key = key
self.data = data
@@ -190,18 +133,13 @@ class Grid:
self.renderers = renderers or {}
self.actions = actions or []
self.linked_columns = linked_columns or []
+ self.vue_tagname = vue_tagname
self.config = self.request.wutta_config
self.app = self.config.get_app()
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
-
def get_columns(self):
"""
Returns the official list of column names for the grid, or
@@ -402,207 +340,6 @@ class Grid:
return True
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]
-
- ##############################
- # 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
- ##############################
-
def render_vue_tag(self, **kwargs):
"""
Render the Vue component tag for the grid.
@@ -681,18 +418,17 @@ class Grid:
"""
Returns a list of Vue-compatible data records.
- This calls :meth:`get_visible_data()` but then may modify the
- result, e.g. to add URLs for :attr:`actions` etc.
+ This uses :attr:`data` as the basis, but may add some extra
+ values to each record, e.g. 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. May be the full set of data, or just the
- current page, per :attr:`paginate_on_backend`.
+ component.
"""
- original_data = self.get_visible_data()
+ original_data = self.data or []
# TODO: at some point i thought it was useful to wrangle the
# columns here, but now i can't seem to figure out why..?
@@ -743,22 +479,6 @@ 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 e588450..5e60bab 100644
--- a/src/wuttaweb/templates/grids/vue_template.mako
+++ b/src/wuttaweb/templates/grids/vue_template.mako
@@ -2,25 +2,8 @@
diff --git a/src/wuttaweb/views/base.py b/src/wuttaweb/views/base.py
index bc1e76c..ccdc749 100644
--- a/src/wuttaweb/views/base.py
+++ b/src/wuttaweb/views/base.py
@@ -25,7 +25,6 @@ Base Logic for Views
"""
from pyramid import httpexceptions
-from pyramid.renderers import render_to_response
from wuttaweb import forms, grids
@@ -118,13 +117,3 @@ class View:
correctly no matter what.
"""
return httpexceptions.HTTPFound(location=url, **kwargs)
-
- def json_response(self, context):
- """
- Convenience method to return a JSON response.
-
- :param context: Context data to be rendered as JSON.
-
- :returns: A :term:`response` with JSON content type.
- """
- return render_to_response('json', context, request=self.request)
diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py
index 8a72cc9..1c7518d 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -181,23 +181,6 @@ class MasterView(View):
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:: paginate_on_backend
-
- Boolean indicating whether the grid data for the
- :meth:`index()` view should be paginated on the backend.
- Default is ``True``.
-
- This is used by :meth:`make_model_grid()` to set the grid's
- :attr:`~wuttaweb.grids.base.Grid.paginate_on_backend` flag.
-
.. attribute:: creatable
Boolean indicating whether the view model supports "creating" -
@@ -246,8 +229,6 @@ class MasterView(View):
# features
listable = True
has_grid = True
- paginated = True
- paginate_on_backend = True
creatable = True
viewable = True
editable = True
@@ -294,16 +275,7 @@ class MasterView(View):
}
if self.has_grid:
- grid = self.make_model_grid()
-
- # so-called 'partial' requests get just data, no html
- if self.request.GET.get('partial'):
- context = {'data': grid.get_vue_data()}
- if grid.paginated and grid.paginate_on_backend:
- context['pager_stats'] = grid.get_vue_pager_stats()
- return self.json_response(context)
-
- context['grid'] = grid
+ context['grid'] = self.make_model_grid()
return self.render_to_response('index', context)
@@ -1089,12 +1061,8 @@ class MasterView(View):
kwargs['actions'] = actions
- kwargs.setdefault('paginated', self.paginated)
- kwargs.setdefault('paginate_on_backend', self.paginate_on_backend)
-
grid = self.make_grid(**kwargs)
self.configure_grid(grid)
- grid.load_settings()
return grid
def get_grid_columns(self):
diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py
index 160b1a4..2d15689 100644
--- a/tests/grids/test_base.py
+++ b/tests/grids/test_base.py
@@ -3,16 +3,29 @@
from unittest import TestCase
from unittest.mock import patch
-from paginate import Page
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
from wuttaweb.grids import base
from wuttaweb.forms import FieldList
-from tests.util import WebTestCase
-class TestGrid(WebTestCase):
+class TestGrid(TestCase):
+
+ 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):
return base.Grid(request or self.request, **kwargs)
@@ -131,143 +144,6 @@ class TestGrid(WebTestCase):
self.assertFalse(grid.is_linked('foo'))
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)
-
- ##############################
- # configuration methods
- ##############################
-
- def test_load_settings(self):
- grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True,
- pagesize=20, page=1)
-
- # settings are loaded, applied, saved
- self.assertEqual(grid.page, 1)
- self.assertNotIn('grid.foo.page', self.request.session)
- self.request.GET = {'pagesize': '10', 'page': '2'}
- grid.load_settings()
- self.assertEqual(grid.page, 2)
- self.assertEqual(self.request.session['grid.foo.page'], 2)
-
- # can skip the saving step
- self.request.GET = {'pagesize': '10', 'page': '3'}
- grid.load_settings(store=False)
- self.assertEqual(grid.page, 3)
- self.assertEqual(self.request.session['grid.foo.page'], 2)
-
- # no error for non-paginated grid
- grid = self.make_grid(key='foo', paginated=False)
- grid.load_settings()
- self.assertFalse(grid.paginated)
-
- def test_request_has_settings(self):
- grid = self.make_grid(key='foo')
-
- self.assertFalse(grid.request_has_settings())
-
- with patch.object(self.request, 'GET', new={'pagesize': '20'}):
- self.assertTrue(grid.request_has_settings())
-
- with patch.object(self.request, 'GET', new={'page': '1'}):
- self.assertTrue(grid.request_has_settings())
-
- def test_update_page_settings(self):
- grid = self.make_grid(key='foo')
-
- # settings are updated from session
- settings = {'pagesize': 20, 'page': 1}
- self.request.session['grid.foo.pagesize'] = 10
- self.request.session['grid.foo.page'] = 2
- grid.update_page_settings(settings)
- self.assertEqual(settings['pagesize'], 10)
- self.assertEqual(settings['page'], 2)
-
- # settings are updated from request
- self.request.GET = {'pagesize': '15', 'page': '4'}
- grid.update_page_settings(settings)
- self.assertEqual(settings['pagesize'], 15)
- self.assertEqual(settings['page'], 4)
-
- def test_persist_settings(self):
- grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
-
- # nb. no error if empty settings, but it saves null values
- grid.persist_settings({})
- self.assertIsNone(self.request.session['grid.foo.page'])
-
- # provided values are saved
- grid.persist_settings({'pagesize': 15, 'page': 3})
- self.assertEqual(self.request.session['grid.foo.page'], 3)
-
- ##############################
- # data methods
- ##############################
-
- def test_get_visible_data(self):
- data = [
- {'foo': 1, 'bar': 1},
- {'foo': 2, 'bar': 2},
- {'foo': 3, 'bar': 3},
- {'foo': 4, 'bar': 4},
- {'foo': 5, 'bar': 5},
- {'foo': 6, 'bar': 6},
- {'foo': 7, 'bar': 7},
- {'foo': 8, 'bar': 8},
- {'foo': 9, 'bar': 9},
- ]
- grid = self.make_grid(data=data,
- columns=['foo', 'bar'],
- paginated=True, paginate_on_backend=True,
- pagesize=4, page=2)
- visible = grid.get_visible_data()
- self.assertEqual(len(visible), 4)
- self.assertEqual(visible[0], {'foo': 5, 'bar': 5})
-
- def test_paginate_data(self):
- grid = self.make_grid()
- pager = grid.paginate_data([])
- self.assertIsInstance(pager, Page)
-
- ##############################
- # rendering methods
- ##############################
-
def test_render_vue_tag(self):
grid = self.make_grid(columns=['foo', 'bar'])
html = grid.render_vue_tag()
@@ -321,28 +197,6 @@ class TestGrid(WebTestCase):
data = grid.get_vue_data()
self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])
- def test_get_vue_pager_stats(self):
- data = [
- {'foo': 1, 'bar': 1},
- {'foo': 2, 'bar': 2},
- {'foo': 3, 'bar': 3},
- {'foo': 4, 'bar': 4},
- {'foo': 5, 'bar': 5},
- {'foo': 6, 'bar': 6},
- {'foo': 7, 'bar': 7},
- {'foo': 8, 'bar': 8},
- {'foo': 9, 'bar': 9},
- ]
-
- grid = self.make_grid(columns=['foo', 'bar'], pagesize=4, page=2)
- grid.pager = grid.paginate_data(data)
- stats = grid.get_vue_pager_stats()
- self.assertEqual(stats['item_count'], 9)
- self.assertEqual(stats['items_per_page'], 4)
- self.assertEqual(stats['page'], 2)
- self.assertEqual(stats['first_item'], 5)
- self.assertEqual(stats['last_item'], 8)
-
class TestGridAction(TestCase):
diff --git a/tests/views/test_base.py b/tests/views/test_base.py
index f86fc8f..67f3f93 100644
--- a/tests/views/test_base.py
+++ b/tests/views/test_base.py
@@ -1,56 +1,46 @@
# -*- coding: utf-8; -*-
+from unittest import TestCase
+
+from pyramid import testing
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
-from wuttaweb.views import base as mod
+from wuttjamaican.conf import WuttaConfig
+from wuttaweb.views import base
from wuttaweb.forms import Form
-from wuttaweb.grids import Grid, GridAction
-from tests.util import WebTestCase
+from wuttaweb.grids import Grid
-class TestView(WebTestCase):
+class TestView(TestCase):
- def make_view(self):
- return mod.View(self.request)
+ def setUp(self):
+ self.config = WuttaConfig()
+ self.app = self.config.get_app()
+ self.request = testing.DummyRequest(wutta_config=self.config)
+ self.view = base.View(self.request)
def test_basic(self):
- view = self.make_view()
- self.assertIs(view.request, self.request)
- self.assertIs(view.config, self.config)
- self.assertIs(view.app, self.app)
+ self.assertIs(self.view.request, self.request)
+ self.assertIs(self.view.config, self.config)
+ self.assertIs(self.view.app, self.app)
def test_forbidden(self):
- view = self.make_view()
- error = view.forbidden()
+ error = self.view.forbidden()
self.assertIsInstance(error, HTTPForbidden)
def test_make_form(self):
- view = self.make_view()
- form = view.make_form()
+ form = self.view.make_form()
self.assertIsInstance(form, Form)
def test_make_grid(self):
- view = self.make_view()
- grid = view.make_grid()
+ grid = self.view.make_grid()
self.assertIsInstance(grid, Grid)
- def test_make_grid_action(self):
- view = self.make_view()
- action = view.make_grid_action('view')
- self.assertIsInstance(action, GridAction)
-
def test_notfound(self):
- view = self.make_view()
- error = view.notfound()
+ error = self.view.notfound()
self.assertIsInstance(error, HTTPNotFound)
def test_redirect(self):
- view = self.make_view()
- error = view.redirect('/')
+ error = self.view.redirect('/')
self.assertIsInstance(error, HTTPFound)
self.assertEqual(error.location, '/')
-
- def test_json_response(self):
- view = self.make_view()
- response = view.json_response({'foo': 'bar'})
- self.assertEqual(response.status_code, 200)
diff --git a/tests/views/test_master.py b/tests/views/test_master.py
index 719ddb8..8647b2a 100644
--- a/tests/views/test_master.py
+++ b/tests/views/test_master.py
@@ -747,14 +747,6 @@ class TestMasterView(WebTestCase):
data = [{'name': 'foo', 'value': 'bar'}]
with patch.object(view, 'get_grid_data', return_value=data):
response = view.index()
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.content_type, 'text/html')
-
- # then once more as 'partial' - aka. data only
- self.request.GET = {'partial': '1'}
- response = view.index()
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.content_type, 'application/json')
def test_create(self):
self.pyramid_config.include('wuttaweb.views.common')