feat: move single-column grid sorting logic to wuttaweb

This commit is contained in:
Lance Edgar 2024-08-18 14:05:52 -05:00
parent c95e42bf82
commit ec36df4a34
7 changed files with 475 additions and 200 deletions

View file

@ -39,7 +39,8 @@ from pyramid.renderers import render
from webhelpers2.html import HTML, tags
from paginate_sqlalchemy import SqlalchemyOrmPage
from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction
from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction, SortInfo
from wuttaweb.util import FieldList
from . import filters as gridfilters
from tailbone.db import Session
from tailbone.util import raw_datetime
@ -48,23 +49,17 @@ from tailbone.util import raw_datetime
log = logging.getLogger(__name__)
class FieldList(list):
"""
Convenience wrapper for a field list.
"""
def insert_before(self, field, newfield):
i = self.index(field)
self.insert(i, newfield)
def insert_after(self, field, newfield):
i = self.index(field)
self.insert(i + 1, newfield)
class Grid(WuttaGrid):
"""
Core grid class. In sore need of documentation.
Base class for all grids.
This is now a subclass of
:class:`wuttaweb:wuttaweb.grids.base.Grid`, and exists to add
customizations which have traditionally been part of Tailbone.
Some of these customizations are still undocumented. Some will
eventually be moved to the upstream/parent class, and possibly
some will be removed outright. What docs we have, are shown here.
.. _Buefy docs: https://buefy.org/documentation/table/
@ -206,10 +201,6 @@ class Grid(WuttaGrid):
filters={},
use_byte_string_filters=False,
searchable={},
sortable=False,
sorters={},
default_sortkey=None,
default_sortdir='asc',
checkboxes=False,
checked=None,
check_handler=None,
@ -231,6 +222,20 @@ class Grid(WuttaGrid):
DeprecationWarning, stacklevel=2)
kwargs.setdefault('vue_tagname', kwargs.pop('component'))
if kwargs.get('default_sortkey'):
warnings.warn("default_sortkey param is deprecated for Grid(); "
"please use sort_defaults param instead",
DeprecationWarning, stacklevel=2)
if kwargs.get('default_sortdir'):
warnings.warn("default_sortdir param is deprecated for Grid(); "
"please use sort_defaults param instead",
DeprecationWarning, stacklevel=2)
if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'):
sortkey = kwargs.pop('default_sortkey', None)
sortdir = kwargs.pop('default_sortdir', 'asc')
if sortkey:
kwargs.setdefault('sort_defaults', [(sortkey, sortdir)])
if kwargs.get('pageable'):
warnings.warn("component param is deprecated for Grid(); "
"please use vue_tagname param instead",
@ -284,11 +289,6 @@ class Grid(WuttaGrid):
self.searchable = searchable or {}
self.sortable = sortable
self.sorters = self.make_sorters(sorters)
self.default_sortkey = default_sortkey
self.default_sortdir = default_sortdir
self.checkboxes = checkboxes
self.checked = checked
if self.checked is None:
@ -328,9 +328,7 @@ class Grid(WuttaGrid):
@property
def component(self):
"""
DEPRECATED - use :attr:`vue_tagname` instead.
"""
""" """
warnings.warn("Grid.component is deprecated; "
"please use vue_tagname instead",
DeprecationWarning, stacklevel=2)
@ -338,20 +336,66 @@ class Grid(WuttaGrid):
@property
def component_studly(self):
"""
DEPRECATED - use :attr:`vue_component` instead.
"""
""" """
warnings.warn("Grid.component_studly is deprecated; "
"please use vue_component instead",
DeprecationWarning, stacklevel=2)
return self.vue_component
def get_default_sortkey(self):
""" """
warnings.warn("Grid.default_sortkey is deprecated; "
"please use Grid.sort_defaults instead",
DeprecationWarning, stacklevel=2)
if self.sort_defaults:
return self.sort_defaults[0].sortkey
def set_default_sortkey(self, value):
""" """
warnings.warn("Grid.default_sortkey is deprecated; "
"please use Grid.sort_defaults instead",
DeprecationWarning, stacklevel=2)
if self.sort_defaults:
info = self.sort_defaults[0]
self.sort_defaults[0] = SortInfo(value, info.sortdir)
else:
self.sort_defaults = [SortInfo(value, 'asc')]
default_sortkey = property(get_default_sortkey, set_default_sortkey)
def get_default_sortdir(self):
""" """
warnings.warn("Grid.default_sortdir is deprecated; "
"please use Grid.sort_defaults instead",
DeprecationWarning, stacklevel=2)
if self.sort_defaults:
return self.sort_defaults[0].sortdir
def set_default_sortdir(self, value):
""" """
warnings.warn("Grid.default_sortdir is deprecated; "
"please use Grid.sort_defaults instead",
DeprecationWarning, stacklevel=2)
if self.sort_defaults:
info = self.sort_defaults[0]
self.sort_defaults[0] = SortInfo(info.sortkey, value)
else:
raise ValueError("cannot set default_sortdir without default_sortkey")
default_sortdir = property(get_default_sortdir, set_default_sortdir)
def get_pageable(self):
""" """
warnings.warn("Grid.pageable is deprecated; "
"please use Grid.paginated instead",
DeprecationWarning, stacklevel=2)
return self.paginated
def set_pageable(self, value):
""" """
warnings.warn("Grid.pageable is deprecated; "
"please use Grid.paginated instead",
DeprecationWarning, stacklevel=2)
self.paginated = value
pageable = property(get_pageable, set_pageable)
@ -405,18 +449,30 @@ class Grid(WuttaGrid):
self.joiners[key] = joiner
def set_sorter(self, key, *args, **kwargs):
if len(args) == 1 and args[0] is None:
self.remove_sorter(key)
""" """
if len(args) == 1:
if kwargs:
warnings.warn("kwargs are ignored for Grid.set_sorter(); "
"please refactor your code accordingly",
DeprecationWarning, stacklevel=2)
if args[0] is None:
warnings.warn("specifying None is deprecated for Grid.set_sorter(); "
"please use Grid.remove_sorter() instead",
DeprecationWarning, stacklevel=2)
self.remove_sorter(key)
else:
super().set_sorter(key, args[0])
elif len(args) == 0:
super().set_sorter(key)
else:
warnings.warn("multiple args are deprecated for Grid.set_sorter(); "
"please refactor your code accordingly",
DeprecationWarning, stacklevel=2)
self.sorters[key] = self.make_sorter(*args, **kwargs)
def remove_sorter(self, key):
self.sorters.pop(key, None)
def set_sort_defaults(self, sortkey, sortdir='asc'):
self.default_sortkey = sortkey
self.default_sortdir = sortdir
def set_filter(self, key, *args, **kwargs):
if len(args) == 1 and args[0] is None:
self.remove_filter(key)
@ -731,53 +787,12 @@ class Grid(WuttaGrid):
if filtr.active:
yield filtr
def make_sorters(self, sorters=None):
"""
Returns an initial set of sorters which will be available to the grid.
The grid itself may or may not provide some default sorters, and the
``sorters`` kwarg may contain additions and/or overrides.
"""
sorters, updates = {}, sorters
if self.model_class:
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
sorters[prop.key] = self.make_sorter(prop)
if updates:
sorters.update(updates)
return sorters
def make_sorter(self, model_property):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
class_ = getattr(model_property, 'class_', self.model_class)
column = getattr(class_, model_property.key)
def sorter(query, direction):
# TODO: this seems hacky..normally we expect a true query
# of course, but in some cases it may be a list instead.
# if so then we can't actually sort
if isinstance(query, list):
return query
return query.order_by(getattr(column, direction)())
sorter._class = class_
sorter._column = column
return sorter
def make_simple_sorter(self, key, foldcase=False):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting a data set comprised of dicts, on the given key.
"""
if foldcase:
keyfunc = lambda v: v[key].lower()
else:
keyfunc = lambda v: v[key]
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
""" """
warnings.warn("Grid.make_simple_sorter() is deprecated; "
"please use Grid.make_sorter() instead",
DeprecationWarning, stacklevel=2)
return self.make_sorter(key, foldcase=foldcase)
def get_pagesize_options(self, default=None):
""" """
@ -849,10 +864,17 @@ class Grid(WuttaGrid):
# initial default settings
settings = {}
if self.sortable:
if self.default_sortkey:
if self.sort_defaults:
sort_defaults = self.sort_defaults
if len(sort_defaults) > 1:
log.warning("multiple sort defaults are not yet supported; "
"list will be pruned to first element for '%s' grid: %s",
self.key, sort_defaults)
sort_defaults = [sort_defaults[0]]
sortinfo = sort_defaults[0]
settings['sorters.length'] = 1
settings['sorters.1.key'] = self.default_sortkey
settings['sorters.1.dir'] = self.default_sortdir
settings['sorters.1.key'] = sortinfo.sortkey
settings['sorters.1.dir'] = sortinfo.sortdir
else:
settings['sorters.length'] = 0
if self.paginated:
@ -927,11 +949,12 @@ class Grid(WuttaGrid):
filtr.verb = settings['filter.{}.verb'.format(filtr.key)]
filtr.value = settings['filter.{}.value'.format(filtr.key)]
if self.sortable:
# and self.sort_on_backend:
self.active_sorters = []
for i in range(1, settings['sorters.length'] + 1):
self.active_sorters.append({
'field': settings[f'sorters.{i}.key'],
'order': settings[f'sorters.{i}.dir'],
'key': settings[f'sorters.{i}.key'],
'dir': settings[f'sorters.{i}.dir'],
})
if self.paginated:
self.pagesize = settings['pagesize']
@ -1321,21 +1344,24 @@ class Grid(WuttaGrid):
return data
def sort_data(self, data):
"""
Sort the given query according to current settings, and return the result.
"""
# bail if no sort settings
if not self.active_sorters:
def sort_data(self, data, sorters=None):
""" """
if sorters is None:
sorters = self.active_sorters
if not sorters:
return data
# TODO: is there a better way to check for SA sorting?
if self.model_class:
# sqlalchemy queries require special handling, in case of
# multi-column sorting
if isinstance(data, orm.Query):
# collect actual column sorters for order_by clause
sorters = []
for sorter in self.active_sorters:
sortkey = sorter['field']
query_sorters = []
for sorter in sorters:
sortkey = sorter['key']
sortdir = sorter['dir']
# cannot sort unless we have a sorter callable
sortfunc = self.sorters.get(sortkey)
if not sortfunc:
log.warning("unknown sorter: %s", sorter)
@ -1347,34 +1373,36 @@ class Grid(WuttaGrid):
self.joined.add(sortkey)
# add column/dir to collection
sortdir = sorter['order']
sorters.append(getattr(sortfunc._column, sortdir)())
query_sorters.append(getattr(sortfunc._column, sortdir)())
# apply sorting to query
if sorters:
data = data.order_by(*sorters)
if query_sorters:
data = data.order_by(*query_sorters)
return data
else:
# not a SQLAlchemy grid, custom sorter
# manual sorting; only one column allowed
if len(sorters) != 1:
raise NotImplementedError("mulit-column manual sorting not yet supported")
assert len(self.active_sorters) < 2
# our one and only active sorter
sorter = sorters[0]
sortkey = sorter['key']
sortdir = sorter['dir']
sortkey = self.active_sorters[0]['field']
sortdir = self.active_sorters[0]['order'] or 'asc'
# cannot sort unless we have a sorter callable
sortfunc = self.sorters.get(sortkey)
if not sortfunc:
return data
# Cannot sort unless we have a sort function.
sortfunc = self.sorters.get(sortkey)
if not sortfunc:
return data
# apply joins needed for this sorter
# TODO: is this actually relevant for manual sort?
if sortkey in self.joiners and sortkey not in self.joined:
data = self.joiners[sortkey](data)
self.joined.add(sortkey)
# apply joins needed for this sorter
if sortkey in self.joiners and sortkey not in self.joined:
data = self.joiners[sortkey](data)
self.joined.add(sortkey)
return sortfunc(data, sortdir)
# invoke the sorter
return sortfunc(data, sortdir)
def paginate_data(self, data):
"""
@ -1671,7 +1699,7 @@ class Grid(WuttaGrid):
columns.append({
'field': name,
'label': self.get_label(name),
'sortable': self.sortable and name in self.sorters,
'sortable': self.is_sortable(name),
'visible': name not in self.invisible,
})
return columns