feat: improve grid filter API a bit, support string/bool filters
This commit is contained in:
parent
4525f91c21
commit
f6fb6957e3
6
docs/api/wuttaweb/grids.filters.rst
Normal file
6
docs/api/wuttaweb/grids.filters.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.grids.filters``
|
||||
==========================
|
||||
|
||||
.. automodule:: wuttaweb.grids.filters
|
||||
:members:
|
|
@ -16,6 +16,7 @@
|
|||
forms.widgets
|
||||
grids
|
||||
grids.base
|
||||
grids.filters
|
||||
handler
|
||||
helpers
|
||||
menus
|
||||
|
|
|
@ -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}"
|
||||
|
|
444
src/wuttaweb/grids/filters.py
Normal file
444
src/wuttaweb/grids/filters.py
Normal 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,
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
385
tests/grids/test_filters.py
Normal 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")
|
Loading…
Reference in a new issue