2
0
Fork 0

feat: improve grid filter API a bit, support string/bool filters

This commit is contained in:
Lance Edgar 2024-08-23 12:10:51 -05:00
parent 4525f91c21
commit f6fb6957e3
7 changed files with 919 additions and 271 deletions

View file

@ -0,0 +1,6 @@
``wuttaweb.grids.filters``
==========================
.. automodule:: wuttaweb.grids.filters
:members:

View file

@ -16,6 +16,7 @@
forms.widgets
grids
grids.base
grids.filters
handler
helpers
menus

View file

@ -40,6 +40,7 @@ from webhelpers2.html import HTML
from wuttaweb.db import Session
from wuttaweb.util import FieldList, get_model_fields, make_json_safe
from wuttjamaican.util import UNSPECIFIED
from wuttaweb.grids.filters import default_sqlalchemy_filters, VerbNotSupported
log = logging.getLogger(__name__)
@ -1034,8 +1035,9 @@ class Grid:
def make_filter(self, columninfo, **kwargs):
"""
Create and return a :class:`GridFilter` instance suitable for
use on the given column.
Create and return a
:class:`~wuttaweb.grids.filters.GridFilter` instance suitable
for use on the given column.
Code usually does not need to call this directly. See also
:meth:`set_filter()`, which calls this method automatically.
@ -1043,24 +1045,34 @@ class Grid:
:param columninfo: Can be either a model property (see below),
or a column name.
:returns: A :class:`GridFilter` instance.
:returns: A :class:`~wuttaweb.grids.filters.GridFilter`
instance.
"""
# model_property is required
model_property = None
if isinstance(columninfo, str):
if kwargs.get('model_property'):
model_property = kwargs['model_property']
elif isinstance(columninfo, str):
key = columninfo
if self.model_class:
try:
mapper = sa.inspect(self.model_class)
except sa.exc.NoInspectionAvailable:
pass
else:
model_property = mapper.get_property(key)
model_property = getattr(self.model_class, key, None)
if not model_property:
raise ValueError(f"cannot locate model property for key: {key}")
else:
model_property = columninfo
return GridFilter(self.request, model_property, **kwargs)
# optional factory override
factory = kwargs.pop('factory', None)
if not factory:
typ = model_property.type
factory = default_sqlalchemy_filters.get(type(typ))
if not factory:
factory = default_sqlalchemy_filters[None]
# make filter
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
def set_filter(self, key, filterinfo=None, **kwargs):
"""
@ -1228,7 +1240,7 @@ class Grid:
settings[f'filter.{filtr.key}.active'] = defaults.get('active',
filtr.default_active)
settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
filtr.default_verb)
filtr.get_default_verb())
settings[f'filter.{filtr.key}.value'] = defaults.get('value',
filtr.default_value)
if self.sortable:
@ -1291,7 +1303,7 @@ class Grid:
if self.filterable:
for filtr in self.filters.values():
filtr.active = settings[f'filter.{filtr.key}.active']
filtr.verb = settings[f'filter.{filtr.key}.verb'] or filtr.default_verb
filtr.verb = settings[f'filter.{filtr.key}.verb'] or filtr.get_default_verb()
filtr.value = settings[f'filter.{filtr.key}.value']
# sorting
@ -1531,8 +1543,8 @@ class Grid:
"""
Returns the list of currently active filters.
This inspects each :class:`GridFilter` in :attr:`filters` and
only returns the ones marked active.
This inspects each :class:`~wuttaweb.grids.filters.GridFilter`
in :attr:`filters` and only returns the ones marked active.
"""
return [filtr for filtr in self.filters.values()
if filtr.active]
@ -1848,7 +1860,9 @@ class Grid:
'key': filtr.key,
'active': filtr.active,
'visible': filtr.active,
'verbs': filtr.verbs,
'verbs': filtr.get_verbs(),
'verb_labels': filtr.get_verb_labels(),
'valueless_verbs': filtr.get_valueless_verbs(),
'verb': filtr.verb,
'value': filtr.value,
'label': filtr.label,
@ -2081,164 +2095,3 @@ class GridAction:
return self.url(obj, i)
return self.url
class GridFilter:
"""
Filter option for a grid. Represents both the "features" as well
as "state" for the filter.
:param request: Current :term:`request` object.
:param model_property: Property of a model class, representing the
column by which to filter. For instance,
``model.Person.full_name``.
:param \**kwargs: Any additional kwargs will be set as attributes
on the filter instance.
Filter instances have the following attributes:
.. attribute:: key
Unique key for the filter. This often corresponds to a "column
name" for the grid, but not always.
.. attribute:: label
Display label for the filter field.
.. attribute:: active
Boolean indicating whether the filter is currently active.
See also :attr:`verb` and :attr:`value`.
.. attribute:: verb
Verb for current filter, if :attr:`active` is true.
See also :attr:`value`.
.. attribute:: value
Value for current filter, if :attr:`active` is true.
See also :attr:`verb`.
.. attribute:: default_active
Boolean indicating whether the filter should be active by
default, i.e. when first displaying the grid.
See also :attr:`default_verb` and :attr:`default_value`.
.. attribute:: default_verb
Filter verb to use by default. This will be auto-selected when
the filter is first activated, or when first displaying the
grid if :attr:`default_active` is true.
See also :attr:`default_value`.
.. attribute:: default_value
Filter value to use by default. This will be auto-populated
when the filter is first activated, or when first displaying
the grid if :attr:`default_active` is true.
See also :attr:`default_verb`.
"""
def __init__(
self,
request,
model_property,
label=None,
default_active=False,
default_verb=None,
default_value=None,
**kwargs,
):
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
self.model_property = model_property
self.key = self.model_property.key
self.label = label or self.app.make_title(self.key)
self.default_active = default_active
self.active = self.default_active
self.verbs = ['contains'] # TODO
self.default_verb = default_verb or self.verbs[0]
self.verb = self.default_verb
self.default_value = default_value
self.value = self.default_value
self.__dict__.update(kwargs)
def __repr__(self):
return ("GridFilter("
f"key='{self.key}', "
f"active={self.active}, "
f"verb='{self.verb}', "
f"value={repr(self.value)})")
def apply_filter(self, data, verb=None, value=UNSPECIFIED):
"""
Filter the given data set according to a verb/value pair.
If verb and/or value are not specified, will use :attr:`verb`
and/or :attr:`value` instead.
This method does not directly filter the data; rather it
delegates (based on ``verb``) to some other method. The
latter may choose *not* to filter the data, e.g. if ``value``
is empty, in which case this may return the original data set
unchanged.
:returns: The (possibly) filtered data set.
"""
if verb is None:
verb = self.verb
if not verb:
log.warn("missing verb for '%s' filter, will use default verb: %s",
self.key, self.default_verb)
verb = self.default_verb
if value is UNSPECIFIED:
value = self.value
func = getattr(self, f'filter_{verb}', None)
if not func:
raise VerbNotSupported(verb)
return func(data, value)
def filter_contains(self, query, value):
"""
Filter data with a full 'ILIKE' query.
"""
if value is None or value == '':
return query
criteria = []
for val in value.split():
val = val.replace('_', r'\_')
val = f'%{val}%'
criteria.append(self.model_property.ilike(val))
return query.filter(sa.and_(*criteria))
class VerbNotSupported(Exception):
""" """
def __init__(self, verb):
self.verb = verb
def __str__(self):
return f"unknown filter verb not supported: {self.verb}"

View file

@ -0,0 +1,444 @@
# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Filters
"""
import logging
import sqlalchemy as sa
from wuttjamaican.util import UNSPECIFIED
log = logging.getLogger(__name__)
class VerbNotSupported(Exception):
""" """
def __init__(self, verb):
self.verb = verb
def __str__(self):
return f"unknown filter verb not supported: {self.verb}"
class GridFilter:
"""
Filter option for a grid. Represents both the "features" as well
as "state" for the filter.
:param request: Current :term:`request` object.
:param model_property: Property of a model class, representing the
column by which to filter. For instance,
``model.Person.full_name``.
:param \**kwargs: Any additional kwargs will be set as attributes
on the filter instance.
Filter instances have the following attributes:
.. attribute:: key
Unique key for the filter. This often corresponds to a "column
name" for the grid, but not always.
.. attribute:: label
Display label for the filter field.
.. attribute:: active
Boolean indicating whether the filter is currently active.
See also :attr:`verb` and :attr:`value`.
.. attribute:: verb
Verb for current filter, if :attr:`active` is true.
See also :attr:`value`.
.. attribute:: value
Value for current filter, if :attr:`active` is true.
See also :attr:`verb`.
.. attribute:: default_active
Boolean indicating whether the filter should be active by
default, i.e. when first displaying the grid.
See also :attr:`default_verb` and :attr:`default_value`.
.. attribute:: default_verb
Filter verb to use by default. This will be auto-selected when
the filter is first activated, or when first displaying the
grid if :attr:`default_active` is true.
See also :attr:`default_value`.
.. attribute:: default_value
Filter value to use by default. This will be auto-populated
when the filter is first activated, or when first displaying
the grid if :attr:`default_active` is true.
See also :attr:`default_verb`.
"""
default_verbs = ['equal', 'not_equal']
default_verb_labels = {
'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",
'contains': "contains",
'does_not_contain': "does not contain",
}
valueless_verbs = [
'is_any',
'is_null',
'is_not_null',
'is_true',
'is_false',
]
def __init__(
self,
request,
key,
label=None,
verbs=None,
default_active=False,
default_verb=None,
default_value=None,
**kwargs,
):
self.request = request
self.key = key
self.config = self.request.wutta_config
self.app = self.config.get_app()
self.label = label or self.app.make_title(self.key)
# active
self.default_active = default_active
self.active = self.default_active
# verb
if verbs is not None:
self.verbs = verbs
if default_verb:
self.default_verb = default_verb
# value
self.default_value = default_value
self.value = self.default_value
self.__dict__.update(kwargs)
def __repr__(self):
verb = getattr(self, 'verb', None)
return (f"{self.__class__.__name__}("
f"key='{self.key}', "
f"active={self.active}, "
f"verb={repr(verb)}, "
f"value={repr(self.value)})")
def get_verbs(self):
"""
Returns the list of verbs supported by the filter.
"""
verbs = None
if hasattr(self, 'verbs'):
verbs = self.verbs
else:
verbs = self.default_verbs
if callable(verbs):
verbs = verbs()
verbs = list(verbs)
if self.nullable:
if 'is_null' not in verbs:
verbs.append('is_null')
if 'is_not_null' not in verbs:
verbs.append('is_not_null')
if 'is_any' not in verbs:
verbs.append('is_any')
return verbs
def get_verb_labels(self):
"""
Returns a dict of all defined verb labels.
"""
# TODO: should traverse hierarchy
labels = dict([(verb, verb) for verb in self.get_verbs()])
labels.update(self.default_verb_labels)
return labels
def get_valueless_verbs(self):
"""
Returns a list of verb names which do not need a value.
"""
return self.valueless_verbs
def get_default_verb(self):
"""
Returns the default verb for the filter.
"""
verb = None
if hasattr(self, 'default_verb'):
verb = self.default_verb
elif hasattr(self, 'verb'):
verb = self.verb
if not verb:
verbs = self.get_verbs()
if verbs:
verb = verbs[0]
return verb
def apply_filter(self, data, verb=None, value=UNSPECIFIED):
"""
Filter the given data set according to a verb/value pair.
If verb and/or value are not specified, will use :attr:`verb`
and/or :attr:`value` instead.
This method does not directly filter the data; rather it
delegates (based on ``verb``) to some other method. The
latter may choose *not* to filter the data, e.g. if ``value``
is empty, in which case this may return the original data set
unchanged.
:returns: The (possibly) filtered data set.
"""
if verb is None:
verb = self.verb
if not verb:
verb = self.get_default_verb()
log.warn("missing verb for '%s' filter, will use default verb: %s",
self.key, verb)
# only attempt for known verbs
if verb not in self.get_verbs():
raise VerbNotSupported(verb)
# fallback value
if value is UNSPECIFIED:
value = self.value
# locate filter method
func = getattr(self, f'filter_{verb}', None)
if not func:
raise VerbNotSupported(verb)
# invoke filter method
return func(data, value)
def filter_is_any(self, data, value):
"""
This is a no-op which always ignores the value and returns the
data as-is.
"""
return data
class AlchemyFilter(GridFilter):
"""
Filter option for a grid with SQLAlchemy query data.
This is a subclass of :class:`GridFilter`. It requires a
``model_property`` to know how to filter the query.
:param model_property: Property of a model class, representing the
column by which to filter. For instance,
``model.Person.full_name``.
:param nullable: Boolean indicating whether the filter should
include ``is_null`` and ``is_not_null`` verbs. If not
specified, the column will be inspected and use its nullable
flag.
"""
def __init__(self, *args, **kwargs):
nullable = kwargs.pop('nullable', None)
super().__init__(*args, **kwargs)
self.nullable = nullable
if self.nullable is None:
columns = self.model_property.prop.columns
if len(columns) == 1:
self.nullable = columns[0].nullable
def coerce_value(self, value):
"""
Coerce the given value to the correct type/format for use with
the filter.
Default logic returns value as-is; subclass may override.
"""
return value
def filter_equal(self, query, value):
"""
Filter data with an equal (``=``) condition.
"""
value = self.coerce_value(value)
if value is None:
return query
return query.filter(self.model_property == value)
def filter_not_equal(self, query, value):
"""
Filter data with a not equal (``!=``) condition.
"""
value = self.coerce_value(value)
if value is None:
return query
# sql probably excludes null values from results, but user
# probably does not expect that, so explicitly include them.
return query.filter(sa.or_(
self.model_property == None,
self.model_property != value,
))
def filter_is_null(self, query, value):
"""
Filter data with an ``IS NULL`` query. The value is ignored.
"""
return query.filter(self.model_property == None)
def filter_is_not_null(self, query, value):
"""
Filter data with an ``IS NOT NULL`` query. The value is
ignored.
"""
return query.filter(self.model_property != None)
class StringAlchemyFilter(AlchemyFilter):
"""
SQLAlchemy filter option for a text data column.
Subclass of :class:`AlchemyFilter`.
"""
default_verbs = ['contains', 'does_not_contain',
'equal', 'not_equal']
def coerce_value(self, value):
""" """
if value is not None:
value = str(value)
if value:
return value
def filter_contains(self, query, value):
"""
Filter data with an ``ILIKE`` condition.
"""
value = self.coerce_value(value)
if not value:
return query
criteria = []
for val in value.split():
val = val.replace('_', r'\_')
val = f'%{val}%'
criteria.append(self.model_property.ilike(val))
return query.filter(sa.and_(*criteria))
def filter_does_not_contain(self, query, value):
"""
Filter data with a ``NOT ILIKE`` condition.
"""
value = self.coerce_value(value)
if not value:
return query
criteria = []
for val in value.split():
val = val.replace('_', r'\_')
val = f'%{val}%'
criteria.append(~self.model_property.ilike(val))
# sql probably excludes null values from results, but user
# probably does not expect that, so explicitly include them.
return query.filter(sa.or_(
self.model_property == None,
sa.and_(*criteria)))
class BooleanAlchemyFilter(AlchemyFilter):
"""
SQLAlchemy filter option for a boolean data column.
Subclass of :class:`AlchemyFilter`.
"""
default_verbs = ['is_true', 'is_false']
def coerce_value(self, value):
""" """
if value is not None:
return bool(value)
def filter_is_true(self, query, value):
"""
Filter data with an "is true" condition. The value is
ignored.
"""
return query.filter(self.model_property == True)
def filter_is_false(self, query, value):
"""
Filter data with an "is false" condition. The value is
ignored.
"""
return query.filter(self.model_property == False)
default_sqlalchemy_filters = {
None: AlchemyFilter,
sa.String: StringAlchemyFilter,
sa.Text: StringAlchemyFilter,
sa.Boolean: BooleanAlchemyFilter,
}

View file

@ -90,16 +90,18 @@
<b-select v-model="filter.verb"
class="filter-verb"
@input="focusValue()"
:size="isSmall ? 'is-small' : null">
<option v-for="verb in filter.verbs"
:key="verb"
:value="verb">
{{ verb }}
{{ filter.verb_labels[verb] || verb }}
</option>
</b-select>
<wutta-filter-value v-model="filter.value"
ref="filterValue"
v-show="valuedVerb()"
:is-small="isSmall" />
</div>
@ -116,9 +118,26 @@
methods: {
focusValue: function() {
focusValue() {
this.$refs.filterValue.focus()
}
},
valuedVerb() {
/* return true if the current verb should expose value input(s) */
// if filter has no "valueless" verbs, then all verbs should expose value inputs
if (!this.filter.valueless_verbs) {
return true
}
// if filter *does* have valueless verbs, check if "current" verb is valueless
if (this.filter.valueless_verbs.includes(this.filter.verb)) {
return false
}
// current verb is *not* valueless
return true
},
}
}

View file

@ -3,6 +3,7 @@
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
@ -10,6 +11,7 @@ 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
@ -921,20 +923,38 @@ class TestGrid(WebTestCase):
def test_make_filter(self):
model = self.app.model
# basic
# arg is column name
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter('name')
self.assertIsInstance(filtr, mod.GridFilter)
self.assertIsInstance(filtr, StringAlchemyFilter)
# property
grid = self.make_grid(model_class=model.Setting)
filtr = grid.make_filter(model.Setting.name)
self.assertIsInstance(filtr, mod.GridFilter)
# invalid model class
# 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
@ -1049,6 +1069,9 @@ class TestGrid(WebTestCase):
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()
@ -1421,86 +1444,3 @@ 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")

385
tests/grids/test_filters.py Normal file
View file

@ -0,0 +1,385 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
from unittest.mock import patch
from wuttaweb.grids import filters as mod
from tests.util import WebTestCase
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):
factory = kwargs.pop('factory', mod.GridFilter)
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_constructor(self):
model = self.app.model
# verbs is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'verbs'))
filtr = self.make_filter(model.Setting.name, verbs=['foo', 'bar'])
self.assertEqual(filtr.verbs, ['foo', 'bar'])
# verb is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'verb'))
filtr = self.make_filter(model.Setting.name, verb='foo')
self.assertEqual(filtr.verb, 'foo')
# default verb is not set by default, but can be set
filtr = self.make_filter(model.Setting.name)
self.assertFalse(hasattr(filtr, 'default_verb'))
filtr = self.make_filter(model.Setting.name, default_verb='foo')
self.assertEqual(filtr.default_verb, 'foo')
def test_repr(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.GridFilter)
self.assertEqual(repr(filtr), "GridFilter(key='name', active=False, verb=None, value=None)")
def test_get_verbs(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])
# by default, returns default verbs (plus 'is_any')
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
# default verbs can be a callable
filtr.default_verbs = lambda: ['foo', 'bar']
self.assertEqual(filtr.get_verbs(), ['foo', 'bar', 'is_any'])
# uses filtr.verbs if set
filtr.verbs = ['is_true', 'is_false']
self.assertEqual(filtr.get_verbs(), ['is_true', 'is_false', 'is_any'])
# may add is/null verbs
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter,
nullable=True)
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal',
'is_null', 'is_not_null',
'is_any'])
# filtr.verbs can be a callable
filtr.nullable = False
filtr.verbs = lambda: ['baz', 'blarg']
self.assertEqual(filtr.get_verbs(), ['baz', 'blarg', 'is_any'])
def test_get_default_verb(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.default_verbs, ['equal', 'not_equal'])
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
# returns first verb by default
self.assertEqual(filtr.get_default_verb(), 'equal')
# returns filtr.verb if set
filtr.verb = 'foo'
self.assertEqual(filtr.get_default_verb(), 'foo')
# returns filtr.default_verb if set
# (nb. this overrides filtr.verb since the point of this
# method is to return the *default* verb)
filtr.default_verb = 'bar'
self.assertEqual(filtr.get_default_verb(), 'bar')
def test_get_verb_labels(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
labels = filtr.get_verb_labels()
self.assertIsInstance(labels, dict)
self.assertEqual(labels['equal'], "equal to")
self.assertEqual(labels['not_equal'], "not equal to")
self.assertEqual(labels['is_any'], "is any")
def test_get_valueless_verbs(self):
model = self.app.model
filtr = self.make_filter(model.Setting.name, factory=mod.AlchemyFilter)
self.assertFalse(hasattr(filtr, 'verbs'))
self.assertEqual(filtr.get_verbs(), ['equal', 'not_equal', 'is_any'])
verbs = filtr.get_valueless_verbs()
self.assertIsInstance(verbs, list)
self.assertIn('is_any', verbs)
def test_apply_filter(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value, factory=mod.StringAlchemyFilter)
# default verb used as fallback
# self.assertEqual(filtr.default_verb, 'contains')
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')
filtr.verbs = ['doesnotexist']
self.assertRaises(mod.VerbNotSupported, filtr.apply_filter,
self.sample_query, verb='doesnotexist')
def test_filter_is_any(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
self.assertEqual(self.sample_query.count(), 9)
# nb. value None is ignored
filtered_query = filtr.filter_is_any(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 9)
class TestAlchemyFilter(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': None},
]
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):
factory = kwargs.pop('factory', mod.AlchemyFilter)
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_filter_equal(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
self.assertEqual(self.sample_query.count(), 9)
# not filtered for null value
filtered_query = filtr.filter_equal(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
# nb. by default, *is filtered* by empty string
filtered_query = filtr.filter_equal(self.sample_query, '')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 0)
# filtered by value
filtered_query = filtr.filter_equal(self.sample_query, 'ggg')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 3)
def test_filter_not_equal(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_not_equal(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
# nb. by default, *is filtered* by empty string
filtered_query = filtr.filter_not_equal(self.sample_query, '')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 9)
# filtered by value
filtered_query = filtr.filter_not_equal(self.sample_query, 'ggg')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 6)
def test_filter_is_null(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
self.assertEqual(self.sample_query.count(), 9)
# nb. value None is ignored
filtered_query = filtr.filter_is_null(self.sample_query, None)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 1)
def test_filter_is_not_null(self):
model = self.app.model
filtr = self.make_filter(model.Setting.value)
self.assertEqual(self.sample_query.count(), 9)
# nb. value None is ignored
filtered_query = filtr.filter_is_not_null(self.sample_query, None)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 8)
class TestStringAlchemyFilter(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):
factory = kwargs.pop('factory', mod.StringAlchemyFilter)
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
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)
def test_filter_does_not_contain(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_does_not_contain(self.sample_query, None)
self.assertIs(filtered_query, self.sample_query)
filtered_query = filtr.filter_does_not_contain(self.sample_query, '')
self.assertIs(filtered_query, self.sample_query)
# filtered by value
filtered_query = filtr.filter_does_not_contain(self.sample_query, 'ggg')
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 6)
class TestBooleanAlchemyFilter(WebTestCase):
def setUp(self):
self.setup_web()
model = self.app.model
self.sample_data = [
{'username': 'alice', 'active': True},
{'username': 'bob', 'active': True},
{'username': 'charlie', 'active': False},
]
for user in self.sample_data:
user = model.User(**user)
self.session.add(user)
self.session.commit()
self.sample_query = self.session.query(model.User)
def make_filter(self, model_property, **kwargs):
factory = kwargs.pop('factory', mod.BooleanAlchemyFilter)
kwargs['model_property'] = model_property
return factory(self.request, model_property.key, **kwargs)
def test_coerce_value(self):
model = self.app.model
filtr = self.make_filter(model.User.active)
self.assertIsNone(filtr.coerce_value(None))
self.assertTrue(filtr.coerce_value(True))
self.assertTrue(filtr.coerce_value(1))
self.assertTrue(filtr.coerce_value('1'))
self.assertFalse(filtr.coerce_value(False))
self.assertFalse(filtr.coerce_value(0))
self.assertFalse(filtr.coerce_value(''))
def test_filter_is_true(self):
model = self.app.model
filtr = self.make_filter(model.User.active)
self.assertEqual(self.sample_query.count(), 3)
# nb. value None is ignored
filtered_query = filtr.filter_is_true(self.sample_query, None)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 2)
def test_filter_is_false(self):
model = self.app.model
filtr = self.make_filter(model.User.active)
self.assertEqual(self.sample_query.count(), 3)
# nb. value None is ignored
filtered_query = filtr.filter_is_false(self.sample_query, None)
self.assertIsNot(filtered_query, self.sample_query)
self.assertEqual(filtered_query.count(), 1)
class TestVerbNotSupported(TestCase):
def test_basic(self):
error = mod.VerbNotSupported('equal')
self.assertEqual(str(error), "unknown filter verb not supported: equal")