3
0
Fork 0
wuttaweb/tests/grids/test_base.py

1827 lines
73 KiB
Python
Raw Normal View History

# -*- coding: utf-8; -*-
import datetime
import decimal
from collections import OrderedDict
from enum import Enum
from unittest import TestCase
from unittest.mock import patch, MagicMock
import sqlalchemy as sa
from sqlalchemy import orm
from paginate import Page
from paginate_sqlalchemy import SqlalchemyOrmPage
from pyramid import testing
from wuttjamaican.conf import WuttaConfig
from wuttaweb.grids import base as mod
from wuttaweb.grids.filters import GridFilter, StringAlchemyFilter, default_sqlalchemy_filters
from wuttaweb.util import FieldList
from wuttaweb.forms import Form
from wuttaweb.testing import WebTestCase
class TestGrid(WebTestCase):
def make_grid(self, request=None, **kwargs):
return mod.Grid(request or self.request, **kwargs)
def test_constructor(self):
# empty
grid = self.make_grid()
self.assertIsNone(grid.key)
self.assertEqual(grid.columns, [])
self.assertIsNone(grid.data)
# now with columns
grid = self.make_grid(columns=['foo', 'bar'])
self.assertIsInstance(grid.columns, FieldList)
self.assertEqual(grid.columns, ['foo', 'bar'])
def test_constructor_sorting(self):
model = self.app.model
# defaults, not sortable
grid = self.make_grid()
self.assertFalse(grid.sortable)
self.assertTrue(grid.sort_on_backend)
self.assertEqual(grid.sorters, {})
self.assertEqual(grid.sort_defaults, [])
# defaults, sortable
grid = self.make_grid(sortable=True)
self.assertTrue(grid.sortable)
self.assertTrue(grid.sort_on_backend)
self.assertEqual(grid.sorters, {})
self.assertEqual(grid.sort_defaults, [])
# sorters may be pre-populated
grid = self.make_grid(model_class=model.Setting, sortable=True)
self.assertEqual(len(grid.sorters), 2)
self.assertIn('name', grid.sorters)
self.assertIn('value', grid.sorters)
self.assertEqual(grid.sort_defaults, [])
# sort defaults as str
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_defaults='name')
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])
# sort defaults as tuple
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_defaults=('name', 'desc'))
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])
# sort defaults as list w/ single tuple
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_defaults=[('name', 'desc')])
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])
# multi-column defaults
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_multiple=True,
sort_defaults=[('name', 'desc'), ('value', 'asc')])
self.assertTrue(grid.sort_multiple)
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc'),
mod.SortInfo('value', 'asc')])
# multi-column sort disabled for oruga
self.request.use_oruga = True
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_multiple=True)
self.assertFalse(grid.sort_multiple)
def test_constructor_filtering(self):
model = self.app.model
# defaults, not filterable
grid = self.make_grid()
self.assertFalse(grid.filterable)
self.assertEqual(grid.filters, {})
# defaults, filterable
grid = self.make_grid(filterable=True)
self.assertTrue(grid.filterable)
self.assertEqual(grid.filters, {})
# filters may be pre-populated
with patch.object(mod.Grid, 'make_filter', return_value=42):
grid = self.make_grid(model_class=model.Setting, filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertIn('name', grid.filters)
self.assertIn('value', grid.filters)
# can specify filters
grid = self.make_grid(model_class=model.Setting, filterable=True,
filters={'name': 42})
self.assertTrue(grid.filterable)
self.assertEqual(grid.filters, {'name': 42})
def test_vue_tagname(self):
grid = self.make_grid()
self.assertEqual(grid.vue_tagname, 'wutta-grid')
def test_vue_component(self):
grid = self.make_grid()
self.assertEqual(grid.vue_component, 'WuttaGrid')
def test_get_columns(self):
model = self.app.model
# empty
grid = self.make_grid()
self.assertEqual(grid.columns, [])
self.assertEqual(grid.get_columns(), [])
# explicit
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.columns, ['foo', 'bar'])
self.assertEqual(grid.get_columns(), ['foo', 'bar'])
# derived from model
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.columns, ['name', 'value'])
self.assertEqual(grid.get_columns(), ['name', 'value'])
2024-08-13 21:43:56 -05:00
def test_append(self):
grid = self.make_grid(columns=['one', 'two'])
self.assertEqual(grid.columns, ['one', 'two'])
grid.append('one', 'two', 'three')
self.assertEqual(grid.columns, ['one', 'two', 'three'])
def test_remove(self):
grid = self.make_grid(columns=['one', 'two', 'three', 'four'])
self.assertEqual(grid.columns, ['one', 'two', 'three', 'four'])
grid.remove('two', 'three')
self.assertEqual(grid.columns, ['one', 'four'])
def test_set_label(self):
model = self.app.model
with patch.object(mod.Grid, 'make_filter'):
# nb. filters are MagicMock instances
grid = self.make_grid(model_class=model.Setting, filterable=True)
self.assertEqual(grid.labels, {})
# basic
grid.set_label('name', "NAME COL")
self.assertEqual(grid.labels['name'], "NAME COL")
# can replace label
grid.set_label('name', "Different")
self.assertEqual(grid.labels['name'], "Different")
self.assertEqual(grid.get_label('name'), "Different")
# can update only column, not filter
self.assertEqual(grid.labels, {'name': "Different"})
self.assertIn('name', grid.filters)
self.assertEqual(grid.filters['name'].label, "Different")
grid.set_label('name', "COLUMN ONLY", column_only=True)
self.assertEqual(grid.get_label('name'), "COLUMN ONLY")
self.assertEqual(grid.filters['name'].label, "Different")
def test_get_label(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.labels, {})
# default derived from key
self.assertEqual(grid.get_label('foo'), "Foo")
# can override
grid.set_label('foo', "Different")
self.assertEqual(grid.get_label('foo'), "Different")
def test_set_renderer(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.renderers, {})
def render1(record, key, value):
pass
# basic
grid.set_renderer('foo', render1)
self.assertIs(grid.renderers['foo'], render1)
def render2(record, key, value, extra=None):
return extra
# can pass kwargs to get a partial
grid.set_renderer('foo', render2, extra=42)
self.assertIsNot(grid.renderers['foo'], render2)
self.assertEqual(grid.renderers['foo'](None, None, None), 42)
# can use built-in string shortcut
grid.set_renderer('foo', 'quantity')
obj = MagicMock(foo=42.00)
self.assertEqual(grid.renderers['foo'](obj, 'foo', 42.00), '42')
def test_set_default_renderers(self):
model = self.app.model
# no defaults for "plain" schema
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.renderers, {})
# no defaults for "plain" mapped class
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.renderers, {})
def myrender(obj, key, value):
return value
# renderer set for datetime mapped field
grid = self.make_grid(model_class=model.Upgrade)
self.assertIn('created', grid.renderers)
self.assertIsNot(grid.renderers['created'], myrender)
# renderer *not* set for datetime, if override present
grid = self.make_grid(model_class=model.Upgrade,
renderers={'created': myrender})
self.assertIn('created', grid.renderers)
self.assertIs(grid.renderers['created'], myrender)
# renderer set for boolean mapped field
grid = self.make_grid(model_class=model.Upgrade)
self.assertIn('executing', grid.renderers)
self.assertIsNot(grid.renderers['executing'], myrender)
# renderer *not* set for boolean, if override present
grid = self.make_grid(model_class=model.Upgrade,
renderers={'executing': myrender})
self.assertIn('executing', grid.renderers)
self.assertIs(grid.renderers['executing'], myrender)
# nb. as of writing we have no Date columns in default schema,
# so must invent one to test that type
class SomeFoolery(model.Base):
__tablename__ = 'somefoolery'
id = sa.Column(sa.Integer(), primary_key=True)
created = sa.Column(sa.Date())
# renderer set for date mapped field
grid = self.make_grid(model_class=SomeFoolery)
self.assertIn('created', grid.renderers)
self.assertIsNot(grid.renderers['created'], myrender)
def test_set_enum(self):
model = self.app.model
class MockEnum(Enum):
FOO = 'foo'
BAR = 'bar'
# no enums by default
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.enums, {})
# enum is set, but not filter choices
grid = self.make_grid(columns=['foo', 'bar'],
filterable=False,
enums={'foo': MockEnum})
self.assertIs(grid.enums['foo'], MockEnum)
self.assertEqual(grid.filters, {})
# both enum and filter choices are set
grid = self.make_grid(model_class=model.Setting,
filterable=True,
enums={'name': MockEnum})
self.assertIs(grid.enums['name'], MockEnum)
self.assertIn('name', grid.filters)
self.assertIn('value', grid.filters)
self.assertEqual(grid.filters['name'].choices, OrderedDict([
('FOO', 'foo'),
('BAR', 'bar'),
]))
def test_linked_columns(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.linked_columns, [])
self.assertFalse(grid.is_linked('foo'))
grid.set_link('foo')
self.assertEqual(grid.linked_columns, ['foo'])
self.assertTrue(grid.is_linked('foo'))
self.assertFalse(grid.is_linked('bar'))
grid.set_link('bar')
self.assertEqual(grid.linked_columns, ['foo', 'bar'])
self.assertTrue(grid.is_linked('foo'))
self.assertTrue(grid.is_linked('bar'))
grid.set_link('foo', False)
self.assertEqual(grid.linked_columns, ['bar'])
self.assertFalse(grid.is_linked('foo'))
self.assertTrue(grid.is_linked('bar'))
def test_hidden_columns(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.hidden_columns, [])
self.assertFalse(grid.is_hidden('foo'))
grid.set_hidden('foo')
self.assertEqual(grid.hidden_columns, ['foo'])
self.assertTrue(grid.is_hidden('foo'))
self.assertFalse(grid.is_hidden('bar'))
grid.set_hidden('bar')
self.assertEqual(grid.hidden_columns, ['foo', 'bar'])
self.assertTrue(grid.is_hidden('foo'))
self.assertTrue(grid.is_hidden('bar'))
grid.set_hidden('foo', False)
self.assertEqual(grid.hidden_columns, ['bar'])
self.assertFalse(grid.is_hidden('foo'))
self.assertTrue(grid.is_hidden('bar'))
def test_searchable_columns(self):
grid = self.make_grid(columns=['foo', 'bar'])
self.assertEqual(grid.searchable_columns, set())
self.assertFalse(grid.is_searchable('foo'))
grid.set_searchable('foo')
self.assertEqual(grid.searchable_columns, {'foo'})
self.assertTrue(grid.is_searchable('foo'))
self.assertFalse(grid.is_searchable('bar'))
grid.set_searchable('bar')
self.assertEqual(grid.searchable_columns, {'foo', 'bar'})
self.assertTrue(grid.is_searchable('foo'))
self.assertTrue(grid.is_searchable('bar'))
grid.set_searchable('foo', False)
self.assertEqual(grid.searchable_columns, {'bar'})
self.assertFalse(grid.is_searchable('foo'))
self.assertTrue(grid.is_searchable('bar'))
def test_add_action(self):
grid = self.make_grid()
self.assertEqual(len(grid.actions), 0)
grid.add_action('view')
self.assertEqual(len(grid.actions), 1)
self.assertIsInstance(grid.actions[0], mod.GridAction)
def test_set_tools(self):
grid = self.make_grid()
self.assertEqual(grid.tools, {})
# null
grid.set_tools(None)
self.assertEqual(grid.tools, {})
# empty
grid.set_tools({})
self.assertEqual(grid.tools, {})
# full dict is replaced
grid.tools = {'foo': 'bar'}
self.assertEqual(grid.tools, {'foo': 'bar'})
grid.set_tools({'bar': 'baz'})
self.assertEqual(grid.tools, {'bar': 'baz'})
# can specify as list of html elements
grid.set_tools(['foo', 'bar'])
self.assertEqual(len(grid.tools), 2)
self.assertEqual(list(grid.tools.values()), ['foo', 'bar'])
def test_add_tool(self):
grid = self.make_grid()
self.assertEqual(grid.tools, {})
# with key
grid.add_tool('foo', key='foo')
self.assertEqual(grid.tools, {'foo': 'foo'})
# without key
grid.add_tool('bar')
self.assertEqual(len(grid.tools), 2)
self.assertEqual(list(grid.tools.values()), ['foo', '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):
model = self.app.model
# nb. first use a paging grid
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(persist=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)
# nb. next use a sorting grid
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True)
# settings are loaded, applied, saved
self.assertEqual(grid.sort_defaults, [])
self.assertFalse(hasattr(grid, 'active_sorters'))
self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'}
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
# can skip the saving step
self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'}
grid.load_settings(persist=False)
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
# no error for non-sortable grid
grid = self.make_grid(key='foo', sortable=False)
grid.load_settings()
self.assertFalse(grid.sortable)
# with sort defaults
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_on_backend=True, sort_defaults='name')
self.assertFalse(hasattr(grid, 'active_sorters'))
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
# with multi-column sort defaults
grid = self.make_grid(model_class=model.Setting, sortable=True,
sort_on_backend=True)
grid.sort_defaults = [
mod.SortInfo('name', 'asc'),
mod.SortInfo('value', 'desc'),
]
self.assertFalse(hasattr(grid, 'active_sorters'))
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
# load settings from session when nothing is in request
self.request.GET = {}
self.request.session.invalidate()
self.assertNotIn('grid.settings.sorters.length', self.request.session)
self.request.session['grid.settings.sorters.length'] = 1
self.request.session['grid.settings.sorters.1.key'] = 'name'
self.request.session['grid.settings.sorters.1.dir'] = 'desc'
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True,
paginated=True, paginate_on_backend=True)
self.assertFalse(hasattr(grid, 'active_sorters'))
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
# filter settings are loaded, applied, saved
grid = self.make_grid(key='settings', model_class=model.Setting,
filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertEqual(len(grid.active_filters), 0)
self.assertNotIn('grid.settings.filter.name.active', self.request.session)
self.assertNotIn('grid.settings.filter.value.active', self.request.session)
self.request.GET = {'name': 'john', 'name.verb': 'contains'}
grid.load_settings()
self.assertTrue(grid.filters['name'].active)
self.assertEqual(grid.filters['name'].verb, 'contains')
self.assertEqual(grid.filters['name'].value, 'john')
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
# filter + sort settings are loaded, applied, saved
self.request.session.invalidate()
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertEqual(len(grid.active_filters), 0)
self.assertNotIn('grid.settings.filter.name.active', self.request.session)
self.assertNotIn('grid.settings.filter.value.active', self.request.session)
self.assertNotIn('grid.settings.sorters.length', self.request.session)
self.request.GET = {'name': 'john', 'name.verb': 'contains',
'sort1key': 'name', 'sort1dir': 'asc'}
grid.load_settings()
self.assertTrue(grid.filters['name'].active)
self.assertEqual(grid.filters['name'].verb, 'contains')
self.assertEqual(grid.filters['name'].value, 'john')
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'asc')
# can reset view to defaults
self.request.GET = {'reset-view': 'true'}
grid.load_settings()
self.assertEqual(grid.active_filters, [])
self.assertIsNone(grid.filters['name'].value)
def test_request_has_settings(self):
model = self.app.model
grid = self.make_grid(key='settings', model_class=model.Setting)
# paging
self.assertFalse(grid.request_has_settings('page'))
with patch.object(grid, 'paginated', new=True):
with patch.object(self.request, 'GET', new={'pagesize': '20'}):
self.assertTrue(grid.request_has_settings('page'))
with patch.object(self.request, 'GET', new={'page': '1'}):
self.assertTrue(grid.request_has_settings('page'))
# sorting
self.assertFalse(grid.request_has_settings('sort'))
with patch.object(grid, 'sortable', new=True):
with patch.object(self.request, 'GET', new={'sort1key': 'name'}):
self.assertTrue(grid.request_has_settings('sort'))
# filtering
grid = self.make_grid(key='settings', model_class=model.Setting, filterable=True)
self.assertFalse(grid.request_has_settings('filter'))
with patch.object(grid, 'filterable', new=True):
with patch.object(self.request, 'GET', new={'name': 'john', 'name.verb': 'contains'}):
self.assertTrue(grid.request_has_settings('filter'))
with patch.object(self.request, 'GET', new={'filter': '1'}):
self.assertTrue(grid.request_has_settings('filter'))
def test_get_setting(self):
grid = self.make_grid(key='foo')
settings = {}
# default is null
value = grid.get_setting(settings, 'pagesize')
self.assertIsNone(value)
# can read value from user session
self.request.session['grid.foo.pagesize'] = 15
value = grid.get_setting(settings, 'pagesize', src='session')
self.assertEqual(value, 15)
# string value not normalized
self.request.session['grid.foo.pagesize'] = '15'
value = grid.get_setting(settings, 'pagesize', src='session')
self.assertEqual(value, '15')
self.assertNotEqual(value, 15)
# but can be normalized
self.request.session['grid.foo.pagesize'] = '15'
value = grid.get_setting(settings, 'pagesize', src='session', normalize=int)
self.assertEqual(value, 15)
# can read value from request
self.request.GET = {'pagesize': '25'}
value = grid.get_setting(settings, 'pagesize', src='request', normalize=int)
self.assertEqual(value, 25)
# null when normalization fails
self.request.GET = {'pagesize': 'invalid'}
value = grid.get_setting(settings, 'pagesize', src='request', normalize=int)
self.assertIsNone(value)
# reset
del self.request.session['grid.foo.pagesize']
self.request.GET = {}
# value can come from provided settings
settings['pagesize'] = '35'
value = grid.get_setting(settings, 'pagesize', src='session', normalize=int)
self.assertEqual(value, 35)
def test_update_filter_settings(self):
model = self.app.model
# nothing happens if not filterable
grid = self.make_grid(key='settings', model_class=model.Setting)
settings = {}
self.request.session['grid.settings.filter.name.active'] = True
self.request.session['grid.settings.filter.name.verb'] = 'contains'
self.request.session['grid.settings.filter.name.value'] = 'john'
grid.update_filter_settings(settings, src='session')
self.assertEqual(settings, {})
# nb. now use a filterable grid
grid = self.make_grid(key='settings', model_class=model.Setting,
filterable=True)
# settings are updated from session
settings = {}
self.request.session['grid.settings.filter.name.active'] = True
self.request.session['grid.settings.filter.name.verb'] = 'contains'
self.request.session['grid.settings.filter.name.value'] = 'john'
grid.update_filter_settings(settings, src='session')
self.assertTrue(settings['filter.name.active'])
self.assertEqual(settings['filter.name.verb'], 'contains')
self.assertEqual(settings['filter.name.value'], 'john')
# settings are updated from request
self.request.GET = {'value': 'sally', 'value.verb': 'contains'}
grid.update_filter_settings(settings, src='request')
self.assertFalse(settings['filter.name.active'])
self.assertTrue(settings['filter.value.active'])
self.assertEqual(settings['filter.value.verb'], 'contains')
self.assertEqual(settings['filter.value.value'], 'sally')
def test_update_sort_settings(self):
model = self.app.model
# nothing happens if not sortable
grid = self.make_grid(key='settings', model_class=model.Setting)
settings = {'sorters.length': 0}
self.request.session['grid.settings.sorters.length'] = 1
self.request.session['grid.settings.sorters.1.key'] = 'name'
self.request.session['grid.settings.sorters.1.dir'] = 'asc'
grid.update_sort_settings(settings, src='session')
self.assertEqual(settings['sorters.length'], 0)
# nb. now use a sortable grid
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True)
# settings are updated from session
settings = {'sorters.length': 1, 'sorters.1.key': 'name', 'sorters.1.dir': 'asc'}
self.request.session['grid.settings.sorters.length'] = 1
self.request.session['grid.settings.sorters.1.key'] = 'name'
self.request.session['grid.settings.sorters.1.dir'] = 'asc'
grid.update_sort_settings(settings, src='session')
self.assertEqual(settings['sorters.length'], 1)
self.assertEqual(settings['sorters.1.key'], 'name')
self.assertEqual(settings['sorters.1.dir'], 'asc')
# settings are updated from request
self.request.GET = {'sort1key': 'value', 'sort1dir': 'desc'}
grid.update_sort_settings(settings, src='request')
self.assertEqual(settings['sorters.length'], 1)
self.assertEqual(settings['sorters.1.key'], 'value')
self.assertEqual(settings['sorters.1.dir'], 'desc')
def test_update_page_settings(self):
# nothing happens if not paginated
grid = self.make_grid(key='foo')
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'], 20)
self.assertEqual(settings['page'], 1)
# nb. now use a paginated grid
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
# 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):
model = self.app.model
# nb. start out with paginated-only grid
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
# invalid dest
self.assertRaises(ValueError, grid.persist_settings, {}, dest='doesnotexist')
# nb. no error if empty settings, but it saves null values
grid.persist_settings({}, dest='session')
self.assertIsNone(self.request.session['grid.foo.page'])
# provided values are saved
grid.persist_settings({'pagesize': 15, 'page': 3}, dest='session')
self.assertEqual(self.request.session['grid.foo.page'], 3)
# nb. now switch to sortable-only grid
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True)
# no error if empty settings; does not save values
grid.persist_settings({}, dest='session')
self.assertNotIn('grid.settings.sorters.length', self.request.session)
# provided values are saved
grid.persist_settings({'sorters.length': 2,
'sorters.1.key': 'name',
'sorters.1.dir': 'desc',
'sorters.2.key': 'value',
'sorters.2.dir': 'asc'},
dest='session')
self.assertEqual(self.request.session['grid.settings.sorters.length'], 2)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
self.assertEqual(self.request.session['grid.settings.sorters.2.key'], 'value')
self.assertEqual(self.request.session['grid.settings.sorters.2.dir'], 'asc')
# old values removed when new are saved
grid.persist_settings({'sorters.length': 1,
'sorters.1.key': 'name',
'sorters.1.dir': 'desc'},
dest='session')
self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
self.assertNotIn('grid.settings.sorters.2.key', self.request.session)
self.assertNotIn('grid.settings.sorters.2.dir', self.request.session)
# nb. now switch to filterable-only grid
grid = self.make_grid(key='settings', model_class=model.Setting,
filterable=True)
self.assertIn('name', grid.filters)
self.assertEqual(grid.filters['name'].key, 'name')
# no error if empty settings; does not save values
grid.persist_settings({}, dest='session')
self.assertNotIn('grid.settings.filters.name', self.request.session)
# provided values are saved
grid.persist_settings({'filter.name.active': True,
'filter.name.verb': 'contains',
'filter.name.value': 'john'},
dest='session')
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
##############################
# sorting methods
##############################
def test_make_backend_sorters(self):
model = self.app.model
# default is empty
grid = self.make_grid()
sorters = grid.make_backend_sorters()
self.assertEqual(sorters, {})
# makes sorters if model class
grid = self.make_grid(model_class=model.Setting)
sorters = grid.make_backend_sorters()
self.assertEqual(len(sorters), 2)
self.assertIn('name', sorters)
self.assertIn('value', sorters)
# does not replace supplied sorters
grid = self.make_grid(model_class=model.Setting)
mysorters = {'value': 42}
sorters = grid.make_backend_sorters(mysorters)
self.assertEqual(len(sorters), 2)
self.assertIn('name', sorters)
self.assertIn('value', sorters)
self.assertEqual(sorters['value'], 42)
self.assertEqual(mysorters['value'], 42)
def test_make_sorter(self):
model = self.app.model
sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'three'},
{'name': 'foo4', 'value': 'four'},
{'name': 'foo5', 'value': 'five'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
]
for setting in sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
sample_query = self.session.query(model.Setting)
# plain data
grid = self.make_grid(columns=['name', 'value'])
sorter = grid.make_sorter('name')
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})
# model class, but still plain data
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter('name')
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})
# repeat previous test, w/ model property
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter(model.Setting.name)
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(sorted_data[0], {'name': 'foo9', 'value': 'nine'})
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(sorted_data[0], {'name': 'foo1', 'value': 'ONE'})
# sqlalchemy query
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter('name')
sorted_query = sorter(sample_query, 'desc')
sorted_data = sorted_query.all()
self.assertEqual(dict(sorted_data[0]), {'name': 'foo9', 'value': 'nine'})
sorted_query = sorter(sample_query, 'asc')
sorted_data = sorted_query.all()
self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})
# repeat previous test, w/ model property
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter(model.Setting.name)
sorted_query = sorter(sample_query, 'desc')
sorted_data = sorted_query.all()
self.assertEqual(dict(sorted_data[0]), {'name': 'foo9', 'value': 'nine'})
sorted_query = sorter(sample_query, 'asc')
sorted_data = sorted_query.all()
self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})
# sortfunc for "invalid" column will fail when called; however
# it can work for manual sort w/ custom keyfunc
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter('doesnotexist')
self.assertRaises(TypeError, sorter, sample_query, 'desc')
self.assertRaises(KeyError, sorter, sample_data, 'desc')
sorter = grid.make_sorter('doesnotexist', keyfunc=lambda obj: obj['name'])
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(len(sorted_data), 9)
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(len(sorted_data), 9)
# case folding is on by default
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter('value')
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
# results are different with case folding off
grid = self.make_grid(model_class=model.Setting)
sorter = grid.make_sorter('value', foldcase=False)
sorted_data = sorter(sample_data, 'desc')
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
sorted_data = sorter(sample_data, 'asc')
self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})
def test_set_joiner(self):
# basic
grid = self.make_grid(columns=['foo', 'bar'], sortable=True, sort_on_backend=True)
self.assertEqual(grid.joiners, {})
grid.set_joiner('foo', 42)
self.assertEqual(grid.joiners, {'foo': 42})
def test_remove_joiner(self):
# basic
grid = self.make_grid(columns=['foo', 'bar'], sortable=True, sort_on_backend=True,
joiners={'foo': 42})
self.assertEqual(grid.joiners, {'foo': 42})
grid.remove_joiner('foo')
self.assertEqual(grid.joiners, {})
def test_set_sorter(self):
model = self.app.model
# explicit sortfunc
grid = self.make_grid()
self.assertEqual(grid.sorters, {})
sortfunc = lambda data, direction: data
grid.set_sorter('foo', sortfunc)
self.assertIs(grid.sorters['foo'], sortfunc)
# auto from model property
grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
self.assertEqual(grid.sorters, {})
grid.set_sorter('name', model.Setting.name)
self.assertTrue(callable(grid.sorters['name']))
# auto from column name
grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
self.assertEqual(grid.sorters, {})
grid.set_sorter('name', 'name')
self.assertTrue(callable(grid.sorters['name']))
# auto from key
grid = self.make_grid(model_class=model.Setting, sortable=True, sorters={})
self.assertEqual(grid.sorters, {})
grid.set_sorter('name')
self.assertTrue(callable(grid.sorters['name']))
def test_remove_sorter(self):
model = self.app.model
# basics
grid = self.make_grid(model_class=model.Setting, sortable=True)
self.assertEqual(len(grid.sorters), 2)
self.assertIn('name', grid.sorters)
self.assertIn('value', grid.sorters)
grid.remove_sorter('value')
self.assertNotIn('value', grid.sorters)
def test_set_sort_defaults(self):
model = self.app.model
grid = self.make_grid(model_class=model.Setting, sortable=True)
self.assertEqual(grid.sort_defaults, [])
# can set just sortkey
grid.set_sort_defaults('name')
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])
# can set sortkey, sortdir
grid.set_sort_defaults('name', 'desc')
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])
# can set sortkey, sortdir as tuple
grid.set_sort_defaults(('value', 'asc'))
self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')])
# can set as list
grid.sort_multiple = True
grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')])
self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc'),
mod.SortInfo('name', 'desc')])
# list is pruned if multi-sort disabled
grid.sort_multiple = False
grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')])
self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')])
# error if any other single arg
self.assertRaises(ValueError, grid.set_sort_defaults, 42)
# error if more than 2 args
self.assertRaises(ValueError, grid.set_sort_defaults, 'name', 'asc', 'value', 'desc')
def test_is_sortable(self):
model = self.app.model
# basics, frontend sorting
grid = self.make_grid(model_class=model.Setting, sortable=True, sort_on_backend=False)
self.assertTrue(grid.is_sortable('name'))
self.assertTrue(grid.is_sortable('value'))
grid.remove_sorter('value')
# nb. columns are always sortable for frontend, despite remove_sorter()
self.assertTrue(grid.is_sortable('value'))
# nb. when grid is not sortable, no column is either
grid.sortable = False
self.assertFalse(grid.is_sortable('name'))
# same test but with backend sorting
grid = self.make_grid(model_class=model.Setting, sortable=True, sort_on_backend=True)
self.assertTrue(grid.is_sortable('name'))
self.assertTrue(grid.is_sortable('value'))
grid.remove_sorter('value')
self.assertFalse(grid.is_sortable('value'))
# nb. when grid is not sortable, no column is either
grid.sortable = False
self.assertFalse(grid.is_sortable('name'))
def test_make_backend_filters(self):
model = self.app.model
# default is empty
grid = self.make_grid()
filters = grid.make_backend_filters()
self.assertEqual(filters, {})
# makes filters if model class
with patch.object(mod.Grid, 'make_filter'):
# nb. filters are MagicMock instances
grid = self.make_grid(model_class=model.Setting)
filters = grid.make_backend_filters()
self.assertEqual(len(filters), 2)
self.assertIn('name', filters)
self.assertIn('value', filters)
# does not replace supplied filters
myfilters = {'value': 42}
with patch.object(mod.Grid, 'make_filter'):
# nb. filters are MagicMock instances
grid = self.make_grid(model_class=model.Setting)
filters = grid.make_backend_filters(myfilters)
self.assertEqual(len(filters), 2)
self.assertIn('name', filters)
self.assertIn('value', filters)
self.assertEqual(filters['value'], 42)
self.assertEqual(myfilters['value'], 42)
# filters for all *true* columns by default, despite grid.columns
with patch.object(mod.Grid, 'make_filter'):
# nb. filters are MagicMock instances
grid = self.make_grid(model_class=model.User,
columns=['username', 'person'])
filters = grid.make_backend_filters()
self.assertIn('username', filters)
self.assertIn('active', filters)
# nb. relationship not included by default
self.assertNotIn('person', filters)
# nb. uuid fields not included by default
self.assertNotIn('uuid', filters)
self.assertNotIn('person_uuid', filters)
def test_make_filter(self):
model = self.app.model
# arg is column name
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter('name')
self.assertIsInstance(filtr, StringAlchemyFilter)
# arg is column name, but model class is invalid
grid = self.make_grid(model_class=42)
self.assertRaises(ValueError, grid.make_filter, 'name')
# arg is model property
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter(model.Setting.name)
self.assertIsInstance(filtr, StringAlchemyFilter)
# model property as kwarg
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter(None, model_property=model.Setting.name)
self.assertIsInstance(filtr, StringAlchemyFilter)
# default factory
grid = self.make_grid(model_class=model.Setting)
with patch.dict(default_sqlalchemy_filters, {None: GridFilter}, clear=True):
filtr = grid.make_filter(model.Setting.name)
self.assertIsInstance(filtr, GridFilter)
self.assertNotIsInstance(filtr, StringAlchemyFilter)
# factory override
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter(model.Setting.name, factory=GridFilter)
self.assertIsInstance(filtr, GridFilter)
self.assertNotIsInstance(filtr, StringAlchemyFilter)
def test_set_filter(self):
model = self.app.model
with patch.object(mod.Grid, 'make_filter', return_value=42):
# auto from model property
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.filters, {})
grid.set_filter('name', model.Setting.name)
self.assertIn('name', grid.filters)
# auto from column name
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.filters, {})
grid.set_filter('name', 'name')
self.assertIn('name', grid.filters)
# auto from key
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.filters, {})
grid.set_filter('name')
self.assertIn('name', grid.filters)
# explicit is not yet implemented
grid = self.make_grid(model_class=model.Setting)
self.assertEqual(grid.filters, {})
self.assertRaises(NotImplementedError, grid.set_filter, 'name', lambda q: q)
def test_remove_filter(self):
model = self.app.model
# basics
with patch.object(mod.Grid, 'make_filter'):
# nb. filters are MagicMock instances
grid = self.make_grid(model_class=model.Setting, filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertIn('name', grid.filters)
self.assertIn('value', grid.filters)
grid.remove_filter('value')
self.assertNotIn('value', grid.filters)
def test_set_filter_defaults(self):
model = self.app.model
# empty by default
grid = self.make_grid(model_class=model.Setting, filterable=True)
self.assertEqual(grid.filter_defaults, {})
# can specify via method call
grid.set_filter_defaults(name={'active': True})
self.assertEqual(grid.filter_defaults, {'name': {'active': True}})
# can specify via constructor
grid = self.make_grid(model_class=model.Setting, filterable=True,
filter_defaults={'name': {'active': True}})
self.assertEqual(grid.filter_defaults, {'name': {'active': True}})
##############################
# data methods
##############################
def test_get_visible_data(self):
model = self.app.model
sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'three'},
{'name': 'foo4', 'value': 'four'},
{'name': 'foo5', 'value': 'five'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
]
for setting in sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
sample_query = self.session.query(model.Setting)
# data is sorted and paginated
grid = self.make_grid(model_class=model.Setting,
data=sample_query,
filterable=True,
sortable=True, sort_on_backend=True,
sort_defaults=('name', 'desc'),
paginated=True, paginate_on_backend=True,
pagesize=4, page=2)
grid.load_settings()
# nb. for now the filtering is mocked
with patch.object(grid, 'filter_data') as filter_data:
filter_data.side_effect = lambda q: q
visible = grid.get_visible_data()
filter_data.assert_called_once_with(sample_query)
self.assertEqual([s.name for s in visible], ['foo5', 'foo4', 'foo3', 'foo2'])
def test_filter_data(self):
model = self.app.model
sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
]
for setting in sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
sample_query = self.session.query(model.Setting)
grid = self.make_grid(key='settings', model_class=model.Setting, filterable=True)
self.assertEqual(list(grid.filters), ['name', 'value'])
self.assertIsInstance(grid.filters['name'], StringAlchemyFilter)
self.assertIsInstance(grid.filters['value'], StringAlchemyFilter)
# not filtered by default
grid.load_settings()
self.assertEqual(grid.active_filters, [])
filtered_query = grid.filter_data(sample_query)
self.assertIs(filtered_query, sample_query)
# can be filtered per session settings
self.request.session['grid.settings.filter.value.active'] = True
self.request.session['grid.settings.filter.value.verb'] = 'contains'
self.request.session['grid.settings.filter.value.value'] = 'ggg'
grid.load_settings()
self.assertEqual(len(grid.active_filters), 1)
self.assertEqual(grid.active_filters[0].key, 'value')
filtered_query = grid.filter_data(sample_query)
self.assertIsInstance(filtered_query, orm.Query)
self.assertIsNot(filtered_query, sample_query)
self.assertEqual(filtered_query.count(), 3)
# can be filtered per request settings
self.request.GET = {'value': 's', 'value.verb': 'contains'}
grid.load_settings()
self.assertEqual(len(grid.active_filters), 1)
self.assertEqual(grid.active_filters[0].key, 'value')
filtered_query = grid.filter_data(sample_query)
self.assertIsInstance(filtered_query, orm.Query)
self.assertEqual(filtered_query.count(), 2)
# not filtered if verb is invalid
self.request.GET = {'value': 'ggg', 'value.verb': 'doesnotexist'}
grid.load_settings()
self.assertEqual(len(grid.active_filters), 1)
self.assertEqual(grid.active_filters[0].verb, 'doesnotexist')
filtered_query = grid.filter_data(sample_query)
self.assertIs(filtered_query, sample_query)
self.assertEqual(filtered_query.count(), 9)
# not filtered if error
self.request.GET = {'value': 'ggg', 'value.verb': 'contains'}
grid.load_settings()
self.assertEqual(len(grid.active_filters), 1)
self.assertEqual(grid.active_filters[0].verb, 'contains')
filtered_query = grid.filter_data(sample_query)
self.assertIsNot(filtered_query, sample_query)
self.assertEqual(filtered_query.count(), 3)
with patch.object(grid.active_filters[0], 'filter_contains', side_effect=RuntimeError):
filtered_query = grid.filter_data(sample_query)
self.assertIs(filtered_query, sample_query)
self.assertEqual(filtered_query.count(), 9)
# joiner is invoked
self.assertEqual(len(grid.active_filters), 1)
self.assertEqual(grid.active_filters[0].key, 'value')
joiner = MagicMock(side_effect=lambda q: q)
grid.joiners = {'value': joiner}
grid.joined = set()
filtered_query = grid.filter_data(sample_query)
joiner.assert_called_once_with(sample_query)
self.assertEqual(filtered_query.count(), 3)
def test_sort_data(self):
model = self.app.model
sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
]
for setting in sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
sample_query = self.session.query(model.Setting)
grid = self.make_grid(model_class=model.Setting,
sortable=True, sort_on_backend=True,
sort_defaults=('name', 'desc'))
grid.load_settings()
# can sort a simple list of data
sorted_data = grid.sort_data(sample_data)
self.assertIsInstance(sorted_data, list)
self.assertEqual(len(sorted_data), 9)
self.assertEqual(sorted_data[0]['name'], 'foo9')
self.assertEqual(sorted_data[-1]['name'], 'foo1')
# can also sort a data query
sorted_query = grid.sort_data(sample_query)
self.assertIsInstance(sorted_query, orm.Query)
sorted_data = sorted_query.all()
self.assertEqual(len(sorted_data), 9)
self.assertEqual(sorted_data[0]['name'], 'foo9')
self.assertEqual(sorted_data[-1]['name'], 'foo1')
# cannot sort data if sorter missing in overrides
sorted_data = grid.sort_data(sample_data, sorters=[])
# nb. sorted data is in same order as original sample (not sorted)
self.assertEqual(sorted_data[0]['name'], 'foo1')
self.assertEqual(sorted_data[-1]['name'], 'foo9')
# multi-column sorting for list data
sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})
# multi-column sorting for query
sorted_query = grid.sort_data(sample_query, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})
# cannot sort data if sortfunc is missing for column
grid.remove_sorter('name')
sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
# nb. sorted data is in same order as original sample (not sorted)
self.assertEqual(sorted_data[0]['name'], 'foo1')
self.assertEqual(sorted_data[-1]['name'], 'foo9')
# now try with a joiner
query = self.session.query(model.User)
grid = self.make_grid(model_class=model.User,
data=query,
columns=['username', 'full_name'],
sortable=True, sort_on_backend=True,
sort_defaults='full_name',
joiners={
'full_name': lambda q: q.join(model.Person),
})
grid.set_sorter('full_name', model.Person.full_name)
grid.load_settings()
data = grid.get_visible_data()
self.assertIsInstance(data, orm.Query)
def test_paginate_data(self):
model = self.app.model
sample_data = [
{'name': 'foo1', 'value': 'ONE'},
{'name': 'foo2', 'value': 'two'},
{'name': 'foo3', 'value': 'three'},
{'name': 'foo4', 'value': 'four'},
{'name': 'foo5', 'value': 'five'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
{'name': 'foo9', 'value': 'nine'},
]
for setting in sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
sample_query = self.session.query(model.Setting)
# basic list pager
grid = self.make_grid(paginated=True, paginate_on_backend=True)
pager = grid.paginate_data(sample_data)
self.assertIsInstance(pager, Page)
# basic query pager
grid = self.make_grid(paginated=True, paginate_on_backend=True)
pager = grid.paginate_data(sample_query)
self.assertIsInstance(pager, SqlalchemyOrmPage)
# page is reset to 1 for empty data
self.request.session['grid.foo.page'] = 2
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
grid.load_settings()
self.assertEqual(grid.page, 2)
self.assertEqual(self.request.session['grid.foo.page'], 2)
pager = grid.paginate_data(sample_data)
self.assertEqual(pager.page, 1)
self.assertEqual(grid.page, 1)
self.assertEqual(self.request.session['grid.foo.page'], 1)
##############################
# rendering methods
##############################
def test_render_batch_id(self):
grid = self.make_grid(columns=['foo', 'bar'])
# null
obj = MagicMock(foo=None)
self.assertEqual(grid.render_batch_id(obj, 'foo', None), "")
# int
obj = MagicMock(foo=42)
self.assertEqual(grid.render_batch_id(obj, 'foo', 42), "00000042")
def test_render_boolean(self):
grid = self.make_grid(columns=['foo', 'bar'])
# null
obj = MagicMock(foo=None)
self.assertEqual(grid.render_boolean(obj, 'foo', None), "")
# true
obj = MagicMock(foo=True)
self.assertEqual(grid.render_boolean(obj, 'foo', True), "Yes")
# false
obj = MagicMock(foo=False)
self.assertEqual(grid.render_boolean(obj, 'foo', False), "No")
def test_render_currency(self):
grid = self.make_grid(columns=['foo', 'bar'])
obj = MagicMock()
# null
self.assertEqual(grid.render_currency(obj, 'foo', None), '')
# basic decimal example
value = decimal.Decimal('42.00')
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00')
# basic float example
value = 42.00
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.00')
# decimal places will be rounded
value = decimal.Decimal('42.12345')
self.assertEqual(grid.render_currency(obj, 'foo', value), '$42.12')
# negative numbers get parens
value = decimal.Decimal('-42.42')
self.assertEqual(grid.render_currency(obj, 'foo', value), '($42.42)')
def test_render_enum(self):
enum = self.app.enum
grid = self.make_grid(columns=['foo', 'bar'])
obj = {'status': None}
# null
value = grid.render_enum(obj, 'status', None, enum=enum.UpgradeStatus)
self.assertIsNone(value)
# normal
obj['status'] = enum.UpgradeStatus.SUCCESS
value = grid.render_enum(obj, 'status', 'SUCCESS', enum=enum.UpgradeStatus)
self.assertEqual(value, 'success')
def test_render_percent(self):
grid = self.make_grid(columns=['foo', 'bar'])
obj = MagicMock()
# null
self.assertEqual(grid.render_percent(obj, 'foo', None), "")
# typical
self.assertEqual(grid.render_percent(obj, 'foo', 12.3419), '12.34 %')
# more decimal places
self.assertEqual(grid.render_percent(obj, 'foo', 12.3419, decimals=3), '12.342 %')
self.assertEqual(grid.render_percent(obj, 'foo', 12.3419, decimals=4), '12.3419 %')
# negative
self.assertEqual(grid.render_percent(obj, 'foo', -12.3419), '(12.34 %)')
self.assertEqual(grid.render_percent(obj, 'foo', -12.3419, decimals=3), '(12.342 %)')
def test_render_quantity(self):
grid = self.make_grid(columns=['foo', 'bar'])
obj = MagicMock()
# null
self.assertEqual(grid.render_quantity(obj, 'foo', None), "")
# integer decimals become integers
value = decimal.Decimal('1.000')
self.assertEqual(grid.render_quantity(obj, 'foo', value), "1")
# but decimal places are preserved
value = decimal.Decimal('1.234')
self.assertEqual(grid.render_quantity(obj ,'foo', value), "1.234")
# zero is *not* empty string (with this renderer)
self.assertEqual(grid.render_quantity(obj, 'foo', 0), "0")
def test_render_date(self):
grid = self.make_grid(columns=['foo', 'bar'])
# null
obj = MagicMock(dt=None)
result = grid.render_date(obj, 'dt', None)
2025-01-15 17:03:13 -06:00
self.assertEqual(result, '')
# typical
dt = datetime.date(2025, 1, 13)
obj = MagicMock(dt=dt)
result = grid.render_date(obj, 'dt', str(dt))
self.assertEqual(result, '2025-01-13')
def test_render_datetime(self):
grid = self.make_grid(columns=['foo', 'bar'])
obj = MagicMock(dt=None)
result = grid.render_datetime(obj, 'dt', None)
2025-01-15 17:03:13 -06:00
self.assertEqual(result, '')
dt = datetime.datetime(2024, 12, 12, 13, 44, tzinfo=datetime.timezone.utc)
obj = MagicMock(dt=dt)
result = grid.render_datetime(obj, 'dt', str(dt))
self.assertEqual(result, '2024-12-12 13:44+0000')
def test_render_vue_tag(self):
grid = self.make_grid(columns=['foo', 'bar'])
html = grid.render_vue_tag()
self.assertEqual(html, '<wutta-grid></wutta-grid>')
def test_render_vue_template(self):
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
grid = self.make_grid(columns=['foo', 'bar'])
html = grid.render_vue_template()
self.assertIn('<script type="text/x-template" id="wutta-grid-template">', html)
def test_render_table_element(self):
self.pyramid_config.include('pyramid_mako')
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
'pyramid.events.BeforeRender')
grid = self.make_grid(key='foobar', columns=['foo', 'bar'])
# form not required
html = grid.render_table_element()
self.assertNotIn('<script ', html)
self.assertIn('<b-table ', html)
# form will register grid data
form = Form(self.request)
self.assertEqual(len(form.grid_vue_context), 0)
html = grid.render_table_element(form)
self.assertEqual(len(form.grid_vue_context), 1)
self.assertIn('foobar', form.grid_vue_context)
def test_render_vue_finalize(self):
grid = self.make_grid()
html = grid.render_vue_finalize()
self.assertIn('<script>', html)
self.assertIn("Vue.component('wutta-grid', WuttaGrid)", html)
def test_get_vue_columns(self):
# error if no columns are set
grid = self.make_grid()
self.assertRaises(ValueError, grid.get_vue_columns)
# otherwise get back field/label dicts
grid = self.make_grid(columns=['foo', 'bar'])
columns = grid.get_vue_columns()
first = columns[0]
self.assertEqual(first['field'], 'foo')
self.assertEqual(first['label'], 'Foo')
def test_get_vue_active_sorters(self):
model = self.app.model
# empty
grid = self.make_grid(key='foo', sortable=True, sort_on_backend=True)
grid.load_settings()
sorters = grid.get_vue_active_sorters()
self.assertEqual(sorters, [])
# format is different
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True,
sort_defaults='name')
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
sorters = grid.get_vue_active_sorters()
self.assertEqual(sorters, [{'field': 'name', 'order': 'asc'}])
def test_get_vue_filters(self):
model = self.app.model
# basic
grid = self.make_grid(key='settings', model_class=model.Setting, filterable=True)
grid.load_settings()
filters = grid.get_vue_filters()
self.assertEqual(len(filters), 2)
name, value = filters
self.assertEqual(name['choices'], [])
self.assertEqual(name['choice_labels'], {})
self.assertEqual(value['choices'], [])
self.assertEqual(value['choice_labels'], {})
class MockEnum(Enum):
FOO = 'foo'
BAR = 'bar'
# with filter choices
grid = self.make_grid(key='settings', model_class=model.Setting,
filterable=True, enums={'name': MockEnum})
grid.load_settings()
filters = grid.get_vue_filters()
self.assertEqual(len(filters), 2)
name, value = filters
self.assertEqual(name['choices'], ['FOO', 'BAR'])
self.assertEqual(name['choice_labels'], {'FOO': 'foo', 'BAR': 'bar'})
self.assertEqual(value['choices'], [])
self.assertEqual(value['choice_labels'], {})
def test_object_to_dict(self):
grid = self.make_grid()
setting = {'name': 'foo', 'value': 'bar'}
# new dict but with same values
dct = grid.object_to_dict(setting)
self.assertIsInstance(dct, dict)
self.assertIsNot(dct, setting)
self.assertEqual(dct, setting)
# random object, not iterable
class MockSetting:
def __init__(self, **kw):
self.__dict__.update(kw)
mock = MockSetting(**setting)
dct = grid.object_to_dict(mock)
self.assertIsInstance(dct, dict)
self.assertEqual(dct, setting)
def test_get_vue_context(self):
# empty if no columns defined
grid = self.make_grid()
context = grid.get_vue_context()
self.assertEqual(context, {'data': [], 'row_classes': {}})
# typical data is a list
mydata = [
{'foo': 'bar'},
]
grid = self.make_grid(columns=['foo'], data=mydata)
context = grid.get_vue_context()
self.assertEqual(context, {'data': [{'foo': 'bar'}], 'row_classes': {}})
# if grid has actions, that list may be supplemented
grid.actions.append(mod.GridAction(self.request, 'view', url='/blarg'))
context = grid.get_vue_context()
self.assertIsNot(context['data'], mydata)
self.assertEqual(context, {'data': [{'foo': 'bar', '_action_url_view': '/blarg'}],
'row_classes': {}})
# can override value rendering
grid.set_renderer('foo', lambda record, key, value: "blah blah")
context = grid.get_vue_context()
self.assertEqual(context, {'data': [{'foo': 'blah blah', '_action_url_view': '/blarg'}],
'row_classes': {}})
# can set row class
grid.row_class = 'whatever'
context = grid.get_vue_context()
self.assertEqual(context, {'data': [{'foo': 'blah blah', '_action_url_view': '/blarg'}],
'row_classes': {'0': 'whatever'}})
def test_get_vue_data(self):
# empty if no columns defined
grid = self.make_grid()
data = grid.get_vue_data()
self.assertEqual(data, [])
# typical data is a list
mydata = [
{'foo': 'bar'},
]
grid = self.make_grid(columns=['foo'], data=mydata)
data = grid.get_vue_data()
self.assertEqual(data, [{'foo': 'bar'}])
# if grid has actions, that list may be supplemented
grid.actions.append(mod.GridAction(self.request, 'view', url='/blarg'))
data = grid.get_vue_data()
self.assertIsNot(data, mydata)
self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])
# can override value rendering
grid.set_renderer('foo', lambda record, key, value: "blah blah")
data = grid.get_vue_data()
self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])
def test_get_row_class(self):
model = self.app.model
user = model.User(username='barney', active=True)
self.session.add(user)
self.session.commit()
data = dict(user)
# null by default
grid = self.make_grid()
self.assertIsNone(grid.get_row_class(user, data, 1))
# can use static class
grid.row_class = 'foo'
self.assertEqual(grid.get_row_class(user, data, 1), 'foo')
# can use callable
def status(u, d, i):
if not u.active:
return 'inactive'
grid.row_class = status
self.assertIsNone(grid.get_row_class(user, data, 1))
user.active = False
self.assertEqual(grid.get_row_class(user, data, 1), 'inactive')
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):
def setUp(self):
self.config = WuttaConfig()
self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
def make_action(self, key, **kwargs):
return mod.GridAction(self.request, key, **kwargs)
def test_render_icon(self):
# icon is derived from key by default
action = self.make_action('blarg')
html = action.render_icon()
self.assertIn('<i class="fas fa-blarg">', html)
# oruga has different output
self.request.use_oruga = True
html = action.render_icon()
self.assertIn('<o-icon icon="blarg">', html)
def test_render_label(self):
# label is derived from key by default
action = self.make_action('blarg')
label = action.render_label()
self.assertEqual(label, "Blarg")
# otherwise use what caller provides
action = self.make_action('foo', label="Bar")
label = action.render_label()
self.assertEqual(label, "Bar")
def test_render_icon_and_label(self):
action = self.make_action('blarg')
with patch.multiple(action,
render_icon=lambda: 'ICON',
render_label=lambda: 'LABEL'):
html = action.render_icon_and_label()
self.assertEqual('ICON LABEL', html)
def test_get_url(self):
obj = {'foo': 'bar'}
# null by default
action = self.make_action('blarg')
url = action.get_url(obj)
self.assertIsNone(url)
# or can be "static"
action = self.make_action('blarg', url='/foo')
url = action.get_url(obj)
self.assertEqual(url, '/foo')
# or can be "dynamic"
action = self.make_action('blarg', url=lambda o, i: '/yeehaw')
url = action.get_url(obj)
self.assertEqual(url, '/yeehaw')