feat: add initial filtering logic to grid class
still missing the actual filters, subclass must provide those for now
This commit is contained in:
parent
a042d511fb
commit
9751bf4c2e
|
@ -288,6 +288,28 @@ class Grid:
|
|||
Set of columns declared as searchable for the Vue component.
|
||||
|
||||
See also :meth:`set_searchable()` and :meth:`is_searchable()`.
|
||||
|
||||
.. attribute:: filterable
|
||||
|
||||
Boolean indicating whether the grid should show a "filters"
|
||||
section where user can filter data in various ways. Default is
|
||||
``False``.
|
||||
|
||||
.. attribute:: filters
|
||||
|
||||
Dict of :class:`~wuttaweb.grids.filters.GridFilter` instances
|
||||
available for use with backend filtering.
|
||||
|
||||
Only relevant if :attr:`filterable` is true.
|
||||
|
||||
See also :meth:`set_filter()`.
|
||||
|
||||
.. attribute:: joiners
|
||||
|
||||
Dict of "joiner" functions for use with backend filtering and
|
||||
sorting.
|
||||
|
||||
See :meth:`set_joiner()` for more info.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -313,6 +335,9 @@ class Grid:
|
|||
pagesize=None,
|
||||
page=1,
|
||||
searchable_columns=None,
|
||||
filterable=False,
|
||||
filters=None,
|
||||
joiners=None,
|
||||
):
|
||||
self.request = request
|
||||
self.vue_tagname = vue_tagname
|
||||
|
@ -323,6 +348,7 @@ class Grid:
|
|||
self.renderers = renderers or {}
|
||||
self.actions = actions or []
|
||||
self.linked_columns = linked_columns or []
|
||||
self.joiners = joiners or {}
|
||||
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
|
@ -354,6 +380,15 @@ class Grid:
|
|||
# searching
|
||||
self.searchable_columns = set(searchable_columns or [])
|
||||
|
||||
# filtering
|
||||
self.filterable = filterable
|
||||
if filters is not None:
|
||||
self.filters = filters
|
||||
elif self.filterable:
|
||||
self.filters = self.make_backend_filters()
|
||||
else:
|
||||
self.filters = {}
|
||||
|
||||
def get_columns(self):
|
||||
"""
|
||||
Returns the official list of column names for the grid, or
|
||||
|
@ -442,7 +477,7 @@ class Grid:
|
|||
if key in self.columns:
|
||||
self.columns.remove(key)
|
||||
|
||||
def set_label(self, key, label):
|
||||
def set_label(self, key, label, column_only=False):
|
||||
"""
|
||||
Set/override the label for a column.
|
||||
|
||||
|
@ -450,11 +485,18 @@ class Grid:
|
|||
|
||||
:param label: New label for the column header.
|
||||
|
||||
:param column_only: Boolean indicating whether the label
|
||||
should be applied *only* to the column header (if
|
||||
``True``), vs. applying also to the filter (if ``False``).
|
||||
|
||||
See also :meth:`get_label()`. Label overrides are tracked via
|
||||
:attr:`labels`.
|
||||
"""
|
||||
self.labels[key] = label
|
||||
|
||||
if not column_only and key in self.filters:
|
||||
self.filters[key].label = label
|
||||
|
||||
def get_label(self, key):
|
||||
"""
|
||||
Returns the label text for a given column.
|
||||
|
@ -582,6 +624,63 @@ class Grid:
|
|||
"""
|
||||
self.actions.append(GridAction(self.request, key, **kwargs))
|
||||
|
||||
##############################
|
||||
# joining methods
|
||||
##############################
|
||||
|
||||
def set_joiner(self, key, joiner):
|
||||
"""
|
||||
Set/override the backend joiner for a column.
|
||||
|
||||
A "joiner" is sometimes needed when a column with "related but
|
||||
not primary" data is involved in a sort or filter operation.
|
||||
|
||||
A sorter or filter may need to "join" other table(s) to get at
|
||||
the appropriate data. But if a given column has both a sorter
|
||||
and filter defined, and both are used at the same time, we
|
||||
don't want the join to happen twice.
|
||||
|
||||
Hence we track joiners separately, also keyed by column name
|
||||
(as are sorters and filters). When a column's sorter **and/or**
|
||||
filter is needed, the joiner will be invoked.
|
||||
|
||||
:param key: Name of column.
|
||||
|
||||
:param joiner: A joiner callable, as described below.
|
||||
|
||||
A joiner callable must accept just one ``(data)`` arg and
|
||||
return the "joined" data/query, for example::
|
||||
|
||||
model = app.model
|
||||
grid = Grid(request, model_class=model.Person)
|
||||
|
||||
def join_external_profile_value(query):
|
||||
return query.join(model.ExternalProfile)
|
||||
|
||||
def sort_external_profile(query, direction):
|
||||
sortspec = getattr(model.ExternalProfile.description, direction)
|
||||
return query.order_by(sortspec())
|
||||
|
||||
grid.set_joiner('external_profile', join_external_profile)
|
||||
grid.set_sorter('external_profile', sort_external_profile)
|
||||
|
||||
See also :meth:`remove_joiner()`. Backend joiners are tracked
|
||||
via :attr:`joiners`.
|
||||
"""
|
||||
self.joiners[key] = joiner
|
||||
|
||||
def remove_joiner(self, key):
|
||||
"""
|
||||
Remove the backend joiner for a column.
|
||||
|
||||
Note that this removes the joiner *function*, so there is no
|
||||
way to apply joins for this column unless another joiner is
|
||||
later defined for it.
|
||||
|
||||
See also :meth:`set_joiner()`.
|
||||
"""
|
||||
self.joiners.pop(key, None)
|
||||
|
||||
##############################
|
||||
# sorting methods
|
||||
##############################
|
||||
|
@ -883,6 +982,120 @@ class Grid:
|
|||
return key in self.sorters
|
||||
return True
|
||||
|
||||
##############################
|
||||
# filtering methods
|
||||
##############################
|
||||
|
||||
def make_backend_filters(self, filters=None):
|
||||
"""
|
||||
Make backend filters for all columns in the grid.
|
||||
|
||||
This is called by the constructor, if :attr:`filterable` is
|
||||
true.
|
||||
|
||||
For each column in the grid, this checks the provided
|
||||
``filters`` and if the column is not yet in there, will call
|
||||
:meth:`make_filter()` to add it.
|
||||
|
||||
.. note::
|
||||
|
||||
This only works if grid has a :attr:`model_class`. If not,
|
||||
this method just returns the initial filters (or empty
|
||||
dict).
|
||||
|
||||
:param filters: Optional dict of initial filters. Any
|
||||
existing filters will be left intact, not replaced.
|
||||
|
||||
:returns: Final dict of all filters. Includes any from the
|
||||
initial ``filters`` param as well as any which were
|
||||
created.
|
||||
"""
|
||||
filters = filters or {}
|
||||
|
||||
if self.model_class:
|
||||
for key in self.columns:
|
||||
if key in filters:
|
||||
continue
|
||||
prop = getattr(self.model_class, key, None)
|
||||
if (prop and hasattr(prop, 'property')
|
||||
and isinstance(prop.property, orm.ColumnProperty)):
|
||||
filters[prop.key] = self.make_filter(prop)
|
||||
|
||||
return filters
|
||||
|
||||
def make_filter(self, columninfo, **kwargs):
|
||||
"""
|
||||
Creates and returns a
|
||||
:class:`~wuttaweb.grids.filters.GridFilter` instance suitable
|
||||
for use as a backend filter on the given column.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is not yet implemented; subclass *must*
|
||||
override.
|
||||
|
||||
Code usually does not need to call this directly. See also
|
||||
:meth:`set_filter()`, which calls this method automatically.
|
||||
|
||||
:param columninfo: Can be either a model property (see below),
|
||||
or a column name.
|
||||
|
||||
:returns: A :class:`~wuttaweb.grids.filters.GridFilter`
|
||||
instance suitable for backend sorting.
|
||||
"""
|
||||
if isinstance(columninfo, str):
|
||||
key = columninfo
|
||||
else:
|
||||
model_property = columninfo
|
||||
key = model_property.key
|
||||
|
||||
return GridFilter(self.request, key, **kwargs)
|
||||
|
||||
def set_filter(self, key, filterinfo=None, **kwargs):
|
||||
"""
|
||||
Set/override the backend filter for a column.
|
||||
|
||||
Only relevant if :attr:`filterable` is true.
|
||||
|
||||
:param key: Name of column.
|
||||
|
||||
:param filterinfo: Can be either a
|
||||
:class:`~wuttweb.grids.filters.GridFilter` instance, or
|
||||
else a model property (see below).
|
||||
|
||||
If ``filterinfo`` is a ``GridFilter`` instance, it will be
|
||||
used as-is for the backend filter.
|
||||
|
||||
Otherwise :meth:`make_filter()` will be called to obtain the
|
||||
backend filter. The ``filterinfo`` will be passed along to
|
||||
that call; if it is empty then ``key`` will be used instead.
|
||||
|
||||
See also :meth:`remove_filter()`. Backend filters are tracked
|
||||
via :attr:`filters`.
|
||||
"""
|
||||
filtr = None
|
||||
|
||||
if filterinfo and callable(filterinfo):
|
||||
# filtr = filterinfo
|
||||
raise NotImplementedError
|
||||
else:
|
||||
kwargs.setdefault('label', self.get_label(key))
|
||||
filtr = self.make_filter(filterinfo or key, **kwargs)
|
||||
|
||||
self.filters[key] = filtr
|
||||
|
||||
def remove_filter(self, key):
|
||||
"""
|
||||
Remove the backend filter for a column.
|
||||
|
||||
This removes the filter *instance*, so there is no way to
|
||||
filter by this column unless another filter is later defined
|
||||
for it.
|
||||
|
||||
See also :meth:`set_filter()`.
|
||||
"""
|
||||
self.filters.pop(key, None)
|
||||
|
||||
##############################
|
||||
# paging methods
|
||||
##############################
|
||||
|
@ -973,6 +1186,11 @@ class Grid:
|
|||
|
||||
# initial default settings
|
||||
settings = {}
|
||||
if self.filterable:
|
||||
for filtr in self.filters.values():
|
||||
settings[f'filter.{filtr.key}.active'] = filtr.default_active
|
||||
settings[f'filter.{filtr.key}.verb'] = filtr.default_verb
|
||||
settings[f'filter.{filtr.key}.value'] = filtr.default_value
|
||||
if self.sortable:
|
||||
if self.sort_defaults:
|
||||
# nb. as of writing neither Buefy nor Oruga support a
|
||||
|
@ -987,14 +1205,35 @@ class Grid:
|
|||
settings['pagesize'] = self.pagesize
|
||||
settings['page'] = self.page
|
||||
|
||||
# TODO
|
||||
# # If user has default settings on file, apply those first.
|
||||
# if self.user_has_defaults():
|
||||
# self.apply_user_defaults(settings)
|
||||
|
||||
# TODO
|
||||
# # If request contains instruction to reset to default filters, then we
|
||||
# # can skip the rest of the request/session checks.
|
||||
# if self.request.GET.get('reset-to-default-filters') == 'true':
|
||||
# pass
|
||||
|
||||
# update settings dict based on what we find in the request
|
||||
# and/or user session. always prioritize the former.
|
||||
|
||||
if self.request_has_settings('filter'):
|
||||
self.update_filter_settings(settings, src='request')
|
||||
if self.request_has_settings('sort'):
|
||||
self.update_sort_settings(settings, src='request')
|
||||
else:
|
||||
self.update_sort_settings(settings, src='session')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
elif self.request_has_settings('sort'):
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_sort_settings(settings, src='request')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
elif self.request_has_settings('page'):
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_sort_settings(settings, src='session')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
|
@ -1003,6 +1242,7 @@ class Grid:
|
|||
persist = False
|
||||
|
||||
# but still should load whatever is in user session
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_sort_settings(settings, src='session')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
|
@ -1010,8 +1250,21 @@ class Grid:
|
|||
if persist:
|
||||
self.persist_settings(settings, dest='session')
|
||||
|
||||
# TODO
|
||||
# # If request contained instruction to save current settings as defaults
|
||||
# # for the current user, then do that.
|
||||
# if self.request.GET.get('save-current-filters-as-defaults') == 'true':
|
||||
# self.persist_settings(settings, dest='defaults')
|
||||
|
||||
# update ourself to reflect settings dict..
|
||||
|
||||
# filtering
|
||||
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']
|
||||
filtr.value = settings[f'filter.{filtr.key}.value']
|
||||
|
||||
# sorting
|
||||
if self.sortable:
|
||||
# nb. doing this for frontend sorting also
|
||||
|
@ -1036,11 +1289,18 @@ class Grid:
|
|||
def request_has_settings(self, typ):
|
||||
""" """
|
||||
|
||||
if typ == 'sort':
|
||||
if typ == 'filter' and self.filterable:
|
||||
for filtr in self.filters.values():
|
||||
if filtr.key in self.request.GET:
|
||||
return True
|
||||
if 'filter' in self.request.GET: # user may be applying empty filters
|
||||
return True
|
||||
|
||||
elif typ == 'sort' and self.sortable and self.sort_on_backend:
|
||||
if 'sort1key' in self.request.GET:
|
||||
return True
|
||||
|
||||
elif typ == 'page':
|
||||
elif typ == 'page' and self.paginated and self.paginate_on_backend:
|
||||
for key in ['pagesize', 'page']:
|
||||
if key in self.request.GET:
|
||||
return True
|
||||
|
@ -1072,6 +1332,31 @@ class Grid:
|
|||
# okay then, default it is
|
||||
return default
|
||||
|
||||
def update_filter_settings(self, settings, src=None):
|
||||
""" """
|
||||
if not self.filterable:
|
||||
return
|
||||
|
||||
for filtr in self.filters.values():
|
||||
prefix = f'filter.{filtr.key}'
|
||||
|
||||
if src == 'request':
|
||||
# consider filter active if query string contains a value for it
|
||||
settings[f'{prefix}.active'] = filtr.key in self.request.GET
|
||||
settings[f'{prefix}.verb'] = self.get_setting(
|
||||
settings, f'{filtr.key}.verb', src='request', default='')
|
||||
settings[f'{prefix}.value'] = self.get_setting(
|
||||
settings, filtr.key, src='request', default='')
|
||||
|
||||
elif src == 'session':
|
||||
settings[f'{prefix}.active'] = self.get_setting(
|
||||
settings, f'{prefix}.active', src='session',
|
||||
normalize=lambda v: str(v).lower() == 'true', default=False)
|
||||
settings[f'{prefix}.verb'] = self.get_setting(
|
||||
settings, f'{prefix}.verb', src='session', default='')
|
||||
settings[f'{prefix}.value'] = self.get_setting(
|
||||
settings, f'{prefix}.value', src='session', default='')
|
||||
|
||||
def update_sort_settings(self, settings, src=None):
|
||||
""" """
|
||||
if not (self.sortable and self.sort_on_backend):
|
||||
|
@ -1138,8 +1423,18 @@ class Grid:
|
|||
skey = f'grid.{self.key}.{key}'
|
||||
self.request.session[skey] = value(key)
|
||||
|
||||
# filter settings
|
||||
if self.filterable:
|
||||
|
||||
# always save all filters, with status
|
||||
for filtr in self.filters.values():
|
||||
persist(f'filter.{filtr.key}.active',
|
||||
value=lambda k: 'true' if settings.get(k) else 'false')
|
||||
persist(f'filter.{filtr.key}.verb')
|
||||
persist(f'filter.{filtr.key}.value')
|
||||
|
||||
# sort settings
|
||||
if self.sortable:
|
||||
if self.sortable and self.sort_on_backend:
|
||||
|
||||
# first must clear all sort settings from dest. this is
|
||||
# because number of sort settings will vary, so we delete
|
||||
|
@ -1183,10 +1478,15 @@ class Grid:
|
|||
|
||||
See also these methods which may be called by this one:
|
||||
|
||||
* :meth:`filter_data()`
|
||||
* :meth:`sort_data()`
|
||||
* :meth:`paginate_data()`
|
||||
"""
|
||||
data = self.data or []
|
||||
self.joined = set()
|
||||
|
||||
if self.filterable:
|
||||
data = self.filter_data(data)
|
||||
|
||||
if self.sortable and self.sort_on_backend:
|
||||
data = self.sort_data(data)
|
||||
|
@ -1197,6 +1497,21 @@ class Grid:
|
|||
|
||||
return data
|
||||
|
||||
def filter_data(self, data, filters=None):
|
||||
"""
|
||||
Filter the given data and return the result. This is called
|
||||
by :meth:`get_visible_data()`.
|
||||
|
||||
:param filters: Optional list of filters to use. If not
|
||||
specified, the grid's "active" filters are used.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is not yet implemented; subclass *must*
|
||||
override.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def sort_data(self, data, sorters=None):
|
||||
"""
|
||||
Sort the given data and return the result. This is called by
|
||||
|
@ -1227,6 +1542,11 @@ class Grid:
|
|||
if not sortfunc:
|
||||
return data
|
||||
|
||||
# join appropriate model if needed
|
||||
if sortkey in self.joiners and sortkey not in self.joined:
|
||||
data = self.joiners[sortkey](data)
|
||||
self.joined.add(sortkey)
|
||||
|
||||
# invoke the sorter
|
||||
data = sortfunc(data, sortdir)
|
||||
|
||||
|
@ -1687,3 +2007,26 @@ class GridAction:
|
|||
return self.url(obj, i)
|
||||
|
||||
return self.url
|
||||
|
||||
|
||||
# TODO: this needs plenty of work yet..and probably will move?
|
||||
class GridFilter:
|
||||
""" """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request,
|
||||
key,
|
||||
default_active=False,
|
||||
default_verb=None,
|
||||
default_value=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.request = request
|
||||
self.key = key
|
||||
|
||||
self.default_active = default_active
|
||||
self.default_verb = default_verb
|
||||
self.default_value = default_value
|
||||
|
||||
self.__dict__.update(kwargs)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from sqlalchemy import orm
|
||||
from paginate import Page
|
||||
|
@ -86,6 +86,32 @@ class TestGrid(WebTestCase):
|
|||
sort_multiple=True)
|
||||
self.assertFalse(grid.sort_multiple)
|
||||
|
||||
def test_constructor_filtering(self):
|
||||
model = self.app.model
|
||||
|
||||
# defaults, not filterable
|
||||
grid = self.make_grid()
|
||||
self.assertFalse(grid.filterable)
|
||||
self.assertEqual(grid.filters, {})
|
||||
|
||||
# defaults, filterable
|
||||
grid = self.make_grid(filterable=True)
|
||||
self.assertTrue(grid.filterable)
|
||||
self.assertEqual(grid.filters, {})
|
||||
|
||||
# filters may be pre-populated
|
||||
with patch.object(mod.Grid, 'make_filter', return_value=42):
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||
self.assertEqual(len(grid.filters), 2)
|
||||
self.assertIn('name', grid.filters)
|
||||
self.assertIn('value', grid.filters)
|
||||
|
||||
# can specify filters
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True,
|
||||
filters={'name': 42})
|
||||
self.assertTrue(grid.filterable)
|
||||
self.assertEqual(grid.filters, {'name': 42})
|
||||
|
||||
def test_vue_tagname(self):
|
||||
grid = self.make_grid()
|
||||
self.assertEqual(grid.vue_tagname, 'wutta-grid')
|
||||
|
@ -125,17 +151,28 @@ class TestGrid(WebTestCase):
|
|||
self.assertEqual(grid.columns, ['one', 'four'])
|
||||
|
||||
def test_set_label(self):
|
||||
grid = self.make_grid(columns=['foo', 'bar'])
|
||||
model = self.app.model
|
||||
with patch.object(mod.Grid, 'make_filter'):
|
||||
# nb. filters are MagicMock instances
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||
self.assertEqual(grid.labels, {})
|
||||
|
||||
# basic
|
||||
grid.set_label('foo', "Foo Fighters")
|
||||
self.assertEqual(grid.labels['foo'], "Foo Fighters")
|
||||
grid.set_label('name', "NAME COL")
|
||||
self.assertEqual(grid.labels['name'], "NAME COL")
|
||||
|
||||
# can replace label
|
||||
grid.set_label('foo', "Different")
|
||||
self.assertEqual(grid.labels['foo'], "Different")
|
||||
self.assertEqual(grid.get_label('foo'), "Different")
|
||||
grid.set_label('name', "Different")
|
||||
self.assertEqual(grid.labels['name'], "Different")
|
||||
self.assertEqual(grid.get_label('name'), "Different")
|
||||
|
||||
# can update only column, not filter
|
||||
self.assertEqual(grid.labels, {'name': "Different"})
|
||||
self.assertIn('name', grid.filters)
|
||||
self.assertEqual(grid.filters['name'].label, "Different")
|
||||
grid.set_label('name', "COLUMN ONLY", column_only=True)
|
||||
self.assertEqual(grid.get_label('name'), "COLUMN ONLY")
|
||||
self.assertEqual(grid.filters['name'].label, "Different")
|
||||
|
||||
def test_get_label(self):
|
||||
grid = self.make_grid(columns=['foo', 'bar'])
|
||||
|
@ -342,11 +379,53 @@ class TestGrid(WebTestCase):
|
|||
grid.load_settings()
|
||||
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
|
||||
|
||||
# filter settings are loaded, applied, saved
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||
filterable=True)
|
||||
self.assertEqual(len(grid.filters), 2)
|
||||
self.assertFalse(hasattr(grid.filters['name'], 'active'))
|
||||
self.assertFalse(hasattr(grid.filters['value'], 'active'))
|
||||
self.assertNotIn('grid.settings.filter.name.active', self.request.session)
|
||||
self.assertNotIn('grid.settings.filter.value.active', self.request.session)
|
||||
self.request.GET = {'name': 'john', 'name.verb': 'contains'}
|
||||
grid.load_settings()
|
||||
self.assertTrue(grid.filters['name'].active)
|
||||
self.assertEqual(grid.filters['name'].verb, 'contains')
|
||||
self.assertEqual(grid.filters['name'].value, 'john')
|
||||
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
|
||||
|
||||
# filter + sort settings are loaded, applied, saved
|
||||
self.request.session.invalidate()
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||
sortable=True, filterable=True)
|
||||
self.assertEqual(len(grid.filters), 2)
|
||||
self.assertFalse(hasattr(grid.filters['name'], 'active'))
|
||||
self.assertFalse(hasattr(grid.filters['value'], 'active'))
|
||||
self.assertNotIn('grid.settings.filter.name.active', self.request.session)
|
||||
self.assertNotIn('grid.settings.filter.value.active', self.request.session)
|
||||
self.assertNotIn('grid.settings.sorters.length', self.request.session)
|
||||
self.request.GET = {'name': 'john', 'name.verb': 'contains',
|
||||
'sort1key': 'name', 'sort1dir': 'asc'}
|
||||
grid.load_settings()
|
||||
self.assertTrue(grid.filters['name'].active)
|
||||
self.assertEqual(grid.filters['name'].verb, 'contains')
|
||||
self.assertEqual(grid.filters['name'].value, 'john')
|
||||
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
|
||||
self.assertEqual(self.request.session['grid.settings.sorters.length'], 1)
|
||||
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
|
||||
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'asc')
|
||||
|
||||
def test_request_has_settings(self):
|
||||
grid = self.make_grid(key='foo')
|
||||
model = self.app.model
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting)
|
||||
|
||||
# paging
|
||||
self.assertFalse(grid.request_has_settings('page'))
|
||||
with patch.object(grid, 'paginated', new=True):
|
||||
with patch.object(self.request, 'GET', new={'pagesize': '20'}):
|
||||
self.assertTrue(grid.request_has_settings('page'))
|
||||
with patch.object(self.request, 'GET', new={'page': '1'}):
|
||||
|
@ -354,9 +433,19 @@ class TestGrid(WebTestCase):
|
|||
|
||||
# sorting
|
||||
self.assertFalse(grid.request_has_settings('sort'))
|
||||
with patch.object(grid, 'sortable', new=True):
|
||||
with patch.object(self.request, 'GET', new={'sort1key': 'name'}):
|
||||
self.assertTrue(grid.request_has_settings('sort'))
|
||||
|
||||
# filtering
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting, filterable=True)
|
||||
self.assertFalse(grid.request_has_settings('filter'))
|
||||
with patch.object(grid, 'filterable', new=True):
|
||||
with patch.object(self.request, 'GET', new={'name': 'john', 'name.verb': 'contains'}):
|
||||
self.assertTrue(grid.request_has_settings('filter'))
|
||||
with patch.object(self.request, 'GET', new={'filter': '1'}):
|
||||
self.assertTrue(grid.request_has_settings('filter'))
|
||||
|
||||
def test_get_setting(self):
|
||||
grid = self.make_grid(key='foo')
|
||||
settings = {}
|
||||
|
@ -400,6 +489,40 @@ class TestGrid(WebTestCase):
|
|||
value = grid.get_setting(settings, 'pagesize', src='session', normalize=int)
|
||||
self.assertEqual(value, 35)
|
||||
|
||||
def test_update_filter_settings(self):
|
||||
model = self.app.model
|
||||
|
||||
# nothing happens if not filterable
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting)
|
||||
settings = {}
|
||||
self.request.session['grid.settings.filter.name.active'] = True
|
||||
self.request.session['grid.settings.filter.name.verb'] = 'contains'
|
||||
self.request.session['grid.settings.filter.name.value'] = 'john'
|
||||
grid.update_filter_settings(settings, src='session')
|
||||
self.assertEqual(settings, {})
|
||||
|
||||
# nb. now use a filterable grid
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||
filterable=True)
|
||||
|
||||
# settings are updated from session
|
||||
settings = {}
|
||||
self.request.session['grid.settings.filter.name.active'] = True
|
||||
self.request.session['grid.settings.filter.name.verb'] = 'contains'
|
||||
self.request.session['grid.settings.filter.name.value'] = 'john'
|
||||
grid.update_filter_settings(settings, src='session')
|
||||
self.assertTrue(settings['filter.name.active'])
|
||||
self.assertEqual(settings['filter.name.verb'], 'contains')
|
||||
self.assertEqual(settings['filter.name.value'], 'john')
|
||||
|
||||
# settings are updated from request
|
||||
self.request.GET = {'value': 'sally', 'value.verb': 'contains'}
|
||||
grid.update_filter_settings(settings, src='request')
|
||||
self.assertFalse(settings['filter.name.active'])
|
||||
self.assertTrue(settings['filter.value.active'])
|
||||
self.assertEqual(settings['filter.value.verb'], 'contains')
|
||||
self.assertEqual(settings['filter.value.value'], 'sally')
|
||||
|
||||
def test_update_sort_settings(self):
|
||||
model = self.app.model
|
||||
|
||||
|
@ -510,6 +633,25 @@ class TestGrid(WebTestCase):
|
|||
self.assertNotIn('grid.settings.sorters.2.key', self.request.session)
|
||||
self.assertNotIn('grid.settings.sorters.2.dir', self.request.session)
|
||||
|
||||
# nb. now switch to filterable-only grid
|
||||
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||
filterable=True)
|
||||
self.assertIn('name', grid.filters)
|
||||
self.assertEqual(grid.filters['name'].key, 'name')
|
||||
|
||||
# no error if empty settings; does not save values
|
||||
grid.persist_settings({}, dest='session')
|
||||
self.assertNotIn('grid.settings.filters.name', self.request.session)
|
||||
|
||||
# provided values are saved
|
||||
grid.persist_settings({'filter.name.active': True,
|
||||
'filter.name.verb': 'contains',
|
||||
'filter.name.value': 'john'},
|
||||
dest='session')
|
||||
self.assertTrue(self.request.session['grid.settings.filter.name.active'])
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.verb'], 'contains')
|
||||
self.assertEqual(self.request.session['grid.settings.filter.name.value'], 'john')
|
||||
|
||||
##############################
|
||||
# sorting methods
|
||||
##############################
|
||||
|
@ -629,6 +771,23 @@ class TestGrid(WebTestCase):
|
|||
sorted_data = sorter(sample_data, 'asc')
|
||||
self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'})
|
||||
|
||||
def test_set_joiner(self):
|
||||
|
||||
# basic
|
||||
grid = self.make_grid(columns=['foo', 'bar'], sortable=True, sort_on_backend=True)
|
||||
self.assertEqual(grid.joiners, {})
|
||||
grid.set_joiner('foo', 42)
|
||||
self.assertEqual(grid.joiners, {'foo': 42})
|
||||
|
||||
def test_remove_joiner(self):
|
||||
|
||||
# basic
|
||||
grid = self.make_grid(columns=['foo', 'bar'], sortable=True, sort_on_backend=True,
|
||||
joiners={'foo': 42})
|
||||
self.assertEqual(grid.joiners, {'foo': 42})
|
||||
grid.remove_joiner('foo')
|
||||
self.assertEqual(grid.joiners, {})
|
||||
|
||||
def test_set_sorter(self):
|
||||
model = self.app.model
|
||||
|
||||
|
@ -726,6 +885,89 @@ class TestGrid(WebTestCase):
|
|||
grid.sortable = False
|
||||
self.assertFalse(grid.is_sortable('name'))
|
||||
|
||||
def test_make_backend_filters(self):
|
||||
model = self.app.model
|
||||
|
||||
# default is empty
|
||||
grid = self.make_grid()
|
||||
filters = grid.make_backend_filters()
|
||||
self.assertEqual(filters, {})
|
||||
|
||||
# makes filters if model class
|
||||
with patch.object(mod.Grid, 'make_filter'):
|
||||
# nb. filters are MagicMock instances
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
filters = grid.make_backend_filters()
|
||||
self.assertEqual(len(filters), 2)
|
||||
self.assertIn('name', filters)
|
||||
self.assertIn('value', filters)
|
||||
|
||||
# does not replace supplied filters
|
||||
myfilters = {'value': 42}
|
||||
with patch.object(mod.Grid, 'make_filter'):
|
||||
# nb. filters are MagicMock instances
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
filters = grid.make_backend_filters(myfilters)
|
||||
self.assertEqual(len(filters), 2)
|
||||
self.assertIn('name', filters)
|
||||
self.assertIn('value', filters)
|
||||
self.assertEqual(filters['value'], 42)
|
||||
self.assertEqual(myfilters['value'], 42)
|
||||
|
||||
def test_make_filter(self):
|
||||
model = self.app.model
|
||||
|
||||
# basic
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
filtr = grid.make_filter('name')
|
||||
self.assertIsInstance(filtr, mod.GridFilter)
|
||||
|
||||
# property
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
filtr = grid.make_filter(model.Setting.name)
|
||||
self.assertIsInstance(filtr, mod.GridFilter)
|
||||
|
||||
def test_set_filter(self):
|
||||
model = self.app.model
|
||||
|
||||
with patch.object(mod.Grid, 'make_filter', return_value=42):
|
||||
|
||||
# auto from model property
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
self.assertEqual(grid.filters, {})
|
||||
grid.set_filter('name', model.Setting.name)
|
||||
self.assertIn('name', grid.filters)
|
||||
|
||||
# auto from column name
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
self.assertEqual(grid.filters, {})
|
||||
grid.set_filter('name', 'name')
|
||||
self.assertIn('name', grid.filters)
|
||||
|
||||
# auto from key
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
self.assertEqual(grid.filters, {})
|
||||
grid.set_filter('name')
|
||||
self.assertIn('name', grid.filters)
|
||||
|
||||
# explicit is not yet implemented
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
self.assertEqual(grid.filters, {})
|
||||
self.assertRaises(NotImplementedError, grid.set_filter, 'name', lambda q: q)
|
||||
|
||||
def test_remove_filter(self):
|
||||
model = self.app.model
|
||||
|
||||
# basics
|
||||
with patch.object(mod.Grid, 'make_filter'):
|
||||
# nb. filters are MagicMock instances
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||
self.assertEqual(len(grid.filters), 2)
|
||||
self.assertIn('name', grid.filters)
|
||||
self.assertIn('value', grid.filters)
|
||||
grid.remove_filter('value')
|
||||
self.assertNotIn('value', grid.filters)
|
||||
|
||||
##############################
|
||||
# data methods
|
||||
##############################
|
||||
|
@ -751,14 +993,27 @@ class TestGrid(WebTestCase):
|
|||
# data is sorted and paginated
|
||||
grid = self.make_grid(model_class=model.Setting,
|
||||
data=sample_query,
|
||||
filterable=True,
|
||||
sortable=True, sort_on_backend=True,
|
||||
sort_defaults=('name', 'desc'),
|
||||
paginated=True, paginate_on_backend=True,
|
||||
pagesize=4, page=2)
|
||||
grid.load_settings()
|
||||
# nb. for now the filtering is mocked
|
||||
with patch.object(grid, 'filter_data') as filter_data:
|
||||
filter_data.side_effect = lambda q: q
|
||||
visible = grid.get_visible_data()
|
||||
filter_data.assert_called_once_with(sample_query)
|
||||
self.assertEqual([s.name for s in visible], ['foo5', 'foo4', 'foo3', 'foo2'])
|
||||
|
||||
def test_filter_data(self):
|
||||
model = self.app.model
|
||||
|
||||
query = self.session.query(model.Setting)
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||
grid.load_settings()
|
||||
self.assertRaises(NotImplementedError, grid.filter_data, query)
|
||||
|
||||
def test_sort_data(self):
|
||||
model = self.app.model
|
||||
sample_data = [
|
||||
|
@ -827,6 +1082,21 @@ class TestGrid(WebTestCase):
|
|||
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
||||
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
||||
|
||||
# now try with a joiner
|
||||
query = self.session.query(model.User)
|
||||
grid = self.make_grid(model_class=model.User,
|
||||
data=query,
|
||||
columns=['username', 'full_name'],
|
||||
sortable=True, sort_on_backend=True,
|
||||
sort_defaults='full_name',
|
||||
joiners={
|
||||
'full_name': lambda q: q.join(model.Person),
|
||||
})
|
||||
grid.set_sorter('full_name', model.Person.full_name)
|
||||
grid.load_settings()
|
||||
data = grid.get_visible_data()
|
||||
self.assertIsInstance(data, orm.Query)
|
||||
|
||||
def test_paginate_data(self):
|
||||
model = self.app.model
|
||||
sample_data = [
|
||||
|
|
Loading…
Reference in a new issue