3
0
Fork 0

feat: add initial support for proper grid filters

only "text contains" filter supported so far, more to come as needed
This commit is contained in:
Lance Edgar 2024-08-22 13:50:29 -05:00
parent 9751bf4c2e
commit 1443f5253f
12 changed files with 1060 additions and 223 deletions

View file

@ -383,8 +383,7 @@ class TestGrid(WebTestCase):
grid = self.make_grid(key='settings', model_class=model.Setting,
filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertFalse(hasattr(grid.filters['name'], 'active'))
self.assertFalse(hasattr(grid.filters['value'], 'active'))
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'}
@ -401,8 +400,7 @@ class TestGrid(WebTestCase):
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, filterable=True)
self.assertEqual(len(grid.filters), 2)
self.assertFalse(hasattr(grid.filters['name'], 'active'))
self.assertFalse(hasattr(grid.filters['value'], 'active'))
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)
@ -419,6 +417,12 @@ class TestGrid(WebTestCase):
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)
@ -927,6 +931,10 @@ class TestGrid(WebTestCase):
filtr = grid.make_filter(model.Setting.name)
self.assertIsInstance(filtr, mod.GridFilter)
# invalid model class
grid = self.make_grid(model_class=42)
self.assertRaises(ValueError, grid.make_filter, 'name')
def test_set_filter(self):
model = self.app.model
@ -968,6 +976,22 @@ class TestGrid(WebTestCase):
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
##############################
@ -1008,11 +1032,82 @@ class TestGrid(WebTestCase):
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)
query = self.session.query(model.Setting)
grid = self.make_grid(model_class=model.Setting, filterable=True)
grid = self.make_grid(key='settings', model_class=model.Setting, filterable=True)
# not filtered by default
grid.load_settings()
self.assertRaises(NotImplementedError, grid.filter_data, query)
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
@ -1210,6 +1305,15 @@ class TestGrid(WebTestCase):
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)
def test_get_vue_data(self):
# empty if no columns defined
@ -1317,3 +1421,86 @@ class TestGridAction(TestCase):
action = self.make_action('blarg', url=lambda o, i: '/yeehaw')
url = action.get_url(obj)
self.assertEqual(url, '/yeehaw')
class TestGridFilter(WebTestCase):
def setUp(self):
self.setup_web()
model = self.app.model
self.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 self.sample_data:
self.app.save_setting(self.session, setting['name'], setting['value'])
self.session.commit()
self.sample_query = self.session.query(model.Setting)
def make_filter(self, model_property, **kwargs):
return mod.GridFilter(self.request, model_property, **kwargs)
def test_repr(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name)
self.assertEqual(repr(filtr), "GridFilter(key='name', active=False, verb='contains', value=None)")
def test_apply_filter(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
# default verb used as fallback
self.assertEqual(filtr.default_verb, 'contains')
filtr.verb = None
with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
filtered_query = filtr.apply_filter(self.sample_query, value='foo')
filter_contains.assert_called_once_with(self.sample_query, 'foo')
self.assertIsNone(filtr.verb)
# filter verb used as fallback
filtr.verb = 'equal'
with patch.object(filtr, 'filter_equal', create=True, side_effect=lambda q, v: q) as filter_equal:
filtered_query = filtr.apply_filter(self.sample_query, value='foo')
filter_equal.assert_called_once_with(self.sample_query, 'foo')
# filter value used as fallback
filtr.verb = 'contains'
filtr.value = 'blarg'
with patch.object(filtr, 'filter_contains', side_effect=lambda q, v: q) as filter_contains:
filtered_query = filtr.apply_filter(self.sample_query)
filter_contains.assert_called_once_with(self.sample_query, 'blarg')
# error if invalid verb
self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
self.sample_query, verb='doesnotexist')
def test_filter_contains(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
self.assertEqual(self.sample_query.count(), 9)
# not filtered for empty value
filtered_query = filtr.filter_contains(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
filtered_query = filtr.filter_contains(self.sample_query, '')
self.assertIs(filtered_query, self.sample_query)
# filtered by value
filtered_query = filtr.filter_contains(self.sample_query, 'ggg')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 3)
class TestVerbNotSupported(TestCase):
def test_basic(self):
error = mod.VerbNotSupported('equal')
self.assertEqual(str(error), "unknown filter verb not supported: equal")