# -*- coding: utf-8; -*- 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 tests.util 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']) 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) 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_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_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) 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_vue_tag(self): grid = self.make_grid(columns=['foo', 'bar']) html = grid.render_vue_tag() self.assertEqual(html, '') 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('