feat: move single-column grid sorting logic to wuttaweb
This commit is contained in:
parent
c95e42bf82
commit
ec36df4a34
7 changed files with 475 additions and 200 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue