3
0
Fork 0

fix: add "is false or null" grid filter, for nullable bool columns

This commit is contained in:
Lance Edgar 2024-12-08 18:43:40 -06:00
parent a9caf3f970
commit f68fe26ada
2 changed files with 82 additions and 7 deletions

View file

@ -116,20 +116,22 @@ class GridFilter:
'is_any': "is any",
'equal': "equal to",
'not_equal': "not equal to",
'is_null': "is null",
'is_not_null': "is not null",
'is_true': "is true",
'is_false': "is false",
'is_false_null': "is false or null",
'is_null': "is null",
'is_not_null': "is not null",
'contains': "contains",
'does_not_contain': "does not contain",
}
valueless_verbs = [
'is_any',
'is_null',
'is_not_null',
'is_true',
'is_false',
'is_false_null',
'is_null',
'is_not_null',
]
def __init__(
@ -416,6 +418,27 @@ class BooleanAlchemyFilter(AlchemyFilter):
"""
default_verbs = ['is_true', 'is_false']
def get_verbs(self):
""" """
# get basic verbs from caller, or default list
verbs = getattr(self, 'verbs', self.default_verbs)
if callable(verbs):
verbs = verbs()
verbs = list(verbs)
# add some more if column is nullable
if self.nullable:
for verb in ('is_false_null', 'is_null', 'is_not_null'):
if verb not in verbs:
verbs.append(verb)
# add wildcard
if 'is_any' not in verbs:
verbs.append('is_any')
return verbs
def coerce_value(self, value):
""" """
if value is not None:
@ -435,6 +458,14 @@ class BooleanAlchemyFilter(AlchemyFilter):
"""
return query.filter(self.model_property == False)
def filter_is_false_null(self, query, value):
"""
Filter data with "is false or null" condition. The value is
ignored.
"""
return query.filter(sa.or_(self.model_property == False,
self.model_property == None))
default_sqlalchemy_filters = {
None: AlchemyFilter,

View file

@ -328,9 +328,15 @@ class TestBooleanAlchemyFilter(WebTestCase):
model = self.app.model
self.sample_data = [
{'username': 'alice', 'active': True},
{'username': 'bob', 'active': True},
{'username': 'charlie', 'active': False},
{'username': 'alice',
'prevent_edit': False,
'active': True},
{'username': 'bob',
'prevent_edit': True,
'active': True},
{'username': 'charlie',
'active': False,
'prevent_edit': None},
]
for user in self.sample_data:
user = model.User(**user)
@ -343,6 +349,34 @@ class TestBooleanAlchemyFilter(WebTestCase):
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_get_verbs(self):
model = self.app.model
# bool field, not nullable
filtr = self.make_filter(model.User.active,
factory=mod.BooleanAlchemyFilter,
nullable=False)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
# by default, returns default verbs (plus 'is_any')
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])
# default verbs can be a callable
filtr.default_verbs = lambda: ['foo', 'bar']
self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])
# bool field, *nullable*
filtr = self.make_filter(model.User.active,
factory=mod.BooleanAlchemyFilter,
nullable=True)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['is_true', 'is_false'])
# effective verbs also include is_false_null
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_false_null',
'is_null', 'is_not_null', 'is_any'])
def test_coerce_value(self):
model = self.app.model
filtr = self.make_filter(model.User.active)
@ -377,6 +411,16 @@ class TestBooleanAlchemyFilter(WebTestCase):
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 1)
def test_filter_is_false_null(self):
model = self.app.model
filtr = self.make_filter(model.User.prevent_edit)
self.assertEqual(self.sample_query.count(), 3)
# nb. only one account is marked with "prevent edit"
filtered_query = filtr.filter_is_false_null(self.sample_query, None)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
class TestVerbNotSupported(TestCase):