1
0
Fork 0

Compare commits

...

3 commits

Author SHA1 Message Date
Lance Edgar f9fad67f4a bump: version 0.8.1 → 0.9.0 2024-08-16 22:53:17 -05:00
Lance Edgar d151758c48 feat: add backend pagination support for grids 2024-08-16 22:52:24 -05:00
Lance Edgar dd3d640b1c 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
2024-08-16 18:19:24 -05:00
9 changed files with 710 additions and 54 deletions

View file

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

View file

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

View file

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

View file

@ -2,8 +2,25 @@
<script type="text/x-template" id="${grid.vue_tagname}-template">
<${b}-table :data="data"
:loading="loading"
narrowed
hoverable
:loading="loading">
icon-pack="fas"
## paging
% if grid.paginated:
paginated
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
>
% for column in grid.get_vue_columns():
<${b}-table-column field="${column['field']}"
@ -34,21 +51,165 @@
</${b}-table-column>
% 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
{{ renderNumber(pagerStats.first_item) }}
- {{ renderNumber(pagerStats.last_item) }}
of {{ renderNumber(pagerStats.item_count) }} results;
</span>
<b-select v-model="perPage"
% if grid.paginate_on_backend:
@input="onPageSizeChange"
% endif
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>
</script>
<script>
let ${grid.vue_component} = {
template: '#${grid.vue_tagname}-template',
methods: {},
}
let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
let ${grid.vue_component}Data = {
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},
% if grid.paginate_on_backend:
pagerStats: ${json.dumps(grid.get_vue_pager_stats())|n},
% endif
% endif
}
const ${grid.vue_component} = {
template: '#${grid.vue_tagname}-template',
computed: {
% if not grid.paginate_on_backend:
pagerStats() {
let last = this.currentPage * this.perPage
let first = last - this.perPage + 1
if (last > this.data.length) {
last = this.data.length
}
return {
'item_count': this.data.length,
'items_per_page': this.perPage,
'page': this.currentPage,
'first_item': first,
'last_item': last,
}
},
% endif
},
methods: {
renderNumber(value) {
if (value != undefined) {
return value.toLocaleString('en')
}
},
getBasicParams() {
return {
% if grid.paginated and grid.paginate_on_backend:
pagesize: this.perPage,
page: this.currentPage,
% endif
}
},
async fetchData(params, success, failure) {
if (params === undefined || params === null) {
params = new URLSearchParams(this.getBasicParams())
} else {
params = new URLSearchParams(params)
}
if (!params.has('partial')) {
params.append('partial', true)
}
params = params.toString()
this.loading = true
this.$http.get(`${request.path_url}?${'$'}{params}`).then(response => {
console.log(response)
console.log(response.data)
if (!response.data.error) {
${grid.vue_component}CurrentData = response.data.data
this.data = ${grid.vue_component}CurrentData
% if grid.paginated and grid.paginate_on_backend:
this.pagerStats = response.data.pager_stats
% endif
this.loading = false
if (success) {
success()
}
} else {
this.$buefy.toast.open({
message: data.error,
type: 'is-danger',
duration: 2000, // 4 seconds
})
this.loading = false
if (failure) {
failure()
}
}
})
.catch((error) => {
this.data = []
% if grid.paginated and grid.paginate_on_backend:
this.pagerStats = {}
% endif
this.loading = false
if (failure) {
failure()
}
throw error
})
},
% if grid.paginated:
% if grid.paginate_on_backend:
onPageSizeChange(size) {
this.fetchData()
},
% endif
onPageChange(page) {
this.currentPage = page
% if grid.paginate_on_backend:
this.fetchData()
% endif
},
% endif
},
}
</script>

View file

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

View file

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

View file

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

View file

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

View file

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