diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e22370..dad38d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ 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 e07c98b..9e092c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.8.1"
+version = "0.9.0"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@@ -31,6 +31,7 @@ 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 740607c..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
@@ -61,6 +63,11 @@ 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
@@ -82,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.
@@ -106,15 +116,57 @@ class Grid:
See also :meth:`set_link()` and :meth:`is_linked()`.
- .. attribute:: vue_tagname
+ .. attribute:: paginated
- String name for Vue component tag. By default this is
- ``'wutta-grid'``. See also :meth:`render_vue_tag()`.
+ 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).
"""
def __init__(
self,
request,
+ vue_tagname='wutta-grid',
model_class=None,
key=None,
columns=None,
@@ -123,9 +175,14 @@ class Grid:
renderers={},
actions=[],
linked_columns=[],
- vue_tagname='wutta-grid',
+ paginated=False,
+ paginate_on_backend=True,
+ pagesize_options=None,
+ pagesize=None,
+ page=1,
):
self.request = request
+ self.vue_tagname = vue_tagname
self.model_class = model_class
self.key = key
self.data = data
@@ -133,13 +190,18 @@ 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
@@ -340,6 +402,207 @@ 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.
@@ -418,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..?
@@ -479,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 5e60bab..e588450 100644
--- a/src/wuttaweb/templates/grids/vue_template.mako
+++ b/src/wuttaweb/templates/grids/vue_template.mako
@@ -2,8 +2,25 @@
diff --git a/src/wuttaweb/views/base.py b/src/wuttaweb/views/base.py
index ccdc749..bc1e76c 100644
--- a/src/wuttaweb/views/base.py
+++ b/src/wuttaweb/views/base.py
@@ -25,6 +25,7 @@ Base Logic for Views
"""
from pyramid import httpexceptions
+from pyramid.renderers import render_to_response
from wuttaweb import forms, grids
@@ -117,3 +118,13 @@ 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 1c7518d..8a72cc9 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -181,6 +181,23 @@ 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" -
@@ -229,6 +246,8 @@ class MasterView(View):
# features
listable = True
has_grid = True
+ paginated = True
+ paginate_on_backend = True
creatable = True
viewable = True
editable = True
@@ -275,7 +294,16 @@ class MasterView(View):
}
if self.has_grid:
- context['grid'] = self.make_model_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
return self.render_to_response('index', context)
@@ -1061,8 +1089,12 @@ 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 2d15689..160b1a4 100644
--- a/tests/grids/test_base.py
+++ b/tests/grids/test_base.py
@@ -3,29 +3,16 @@
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(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()
+class TestGrid(WebTestCase):
def make_grid(self, request=None, **kwargs):
return base.Grid(request or self.request, **kwargs)
@@ -144,6 +131,143 @@ class TestGrid(TestCase):
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()
@@ -197,6 +321,28 @@ class TestGrid(TestCase):
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 67f3f93..f86fc8f 100644
--- a/tests/views/test_base.py
+++ b/tests/views/test_base.py
@@ -1,46 +1,56 @@
# -*- coding: utf-8; -*-
-from unittest import TestCase
-
-from pyramid import testing
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
-from wuttjamaican.conf import WuttaConfig
-from wuttaweb.views import base
+from wuttaweb.views import base as mod
from wuttaweb.forms import Form
-from wuttaweb.grids import Grid
+from wuttaweb.grids import Grid, GridAction
+from tests.util import WebTestCase
-class TestView(TestCase):
+class TestView(WebTestCase):
- 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 make_view(self):
+ return mod.View(self.request)
def test_basic(self):
- self.assertIs(self.view.request, self.request)
- self.assertIs(self.view.config, self.config)
- self.assertIs(self.view.app, self.app)
+ view = self.make_view()
+ self.assertIs(view.request, self.request)
+ self.assertIs(view.config, self.config)
+ self.assertIs(view.app, self.app)
def test_forbidden(self):
- error = self.view.forbidden()
+ view = self.make_view()
+ error = view.forbidden()
self.assertIsInstance(error, HTTPForbidden)
def test_make_form(self):
- form = self.view.make_form()
+ view = self.make_view()
+ form = view.make_form()
self.assertIsInstance(form, Form)
def test_make_grid(self):
- grid = self.view.make_grid()
+ view = self.make_view()
+ grid = 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):
- error = self.view.notfound()
+ view = self.make_view()
+ error = view.notfound()
self.assertIsInstance(error, HTTPNotFound)
def test_redirect(self):
- error = self.view.redirect('/')
+ view = self.make_view()
+ error = 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 8647b2a..719ddb8 100644
--- a/tests/views/test_master.py
+++ b/tests/views/test_master.py
@@ -747,6 +747,14 @@ 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')