feat: move single-column grid sorting logic to wuttaweb
This commit is contained in:
parent
c95e42bf82
commit
ec36df4a34
|
@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from wuttaweb.util import get_form_data, make_json_safe
|
from wuttaweb.util import FieldList, get_form_data, make_json_safe
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.util import raw_datetime, render_markdown
|
from tailbone.util import raw_datetime, render_markdown
|
||||||
|
@ -1418,30 +1418,6 @@ class Form(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class FieldList(list):
|
|
||||||
"""
|
|
||||||
Convenience wrapper for a form's field list.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def insert_before(self, field, newfield):
|
|
||||||
if field in self:
|
|
||||||
i = self.index(field)
|
|
||||||
self.insert(i, newfield)
|
|
||||||
else:
|
|
||||||
log.warning("field '%s' not found, will append new field: %s",
|
|
||||||
field, newfield)
|
|
||||||
self.append(newfield)
|
|
||||||
|
|
||||||
def insert_after(self, field, newfield):
|
|
||||||
if field in self:
|
|
||||||
i = self.index(field)
|
|
||||||
self.insert(i + 1, newfield)
|
|
||||||
else:
|
|
||||||
log.warning("field '%s' not found, will append new field: %s",
|
|
||||||
field, newfield)
|
|
||||||
self.append(newfield)
|
|
||||||
|
|
||||||
|
|
||||||
@colander.deferred
|
@colander.deferred
|
||||||
def upload_widget(node, kw):
|
def upload_widget(node, kw):
|
||||||
request = kw['request']
|
request = kw['request']
|
||||||
|
|
|
@ -39,7 +39,8 @@ from pyramid.renderers import render
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
from paginate_sqlalchemy import SqlalchemyOrmPage
|
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 . import filters as gridfilters
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.util import raw_datetime
|
from tailbone.util import raw_datetime
|
||||||
|
@ -48,23 +49,17 @@ from tailbone.util import raw_datetime
|
||||||
log = logging.getLogger(__name__)
|
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):
|
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/
|
.. _Buefy docs: https://buefy.org/documentation/table/
|
||||||
|
|
||||||
|
@ -206,10 +201,6 @@ class Grid(WuttaGrid):
|
||||||
filters={},
|
filters={},
|
||||||
use_byte_string_filters=False,
|
use_byte_string_filters=False,
|
||||||
searchable={},
|
searchable={},
|
||||||
sortable=False,
|
|
||||||
sorters={},
|
|
||||||
default_sortkey=None,
|
|
||||||
default_sortdir='asc',
|
|
||||||
checkboxes=False,
|
checkboxes=False,
|
||||||
checked=None,
|
checked=None,
|
||||||
check_handler=None,
|
check_handler=None,
|
||||||
|
@ -231,6 +222,20 @@ class Grid(WuttaGrid):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('vue_tagname', kwargs.pop('component'))
|
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'):
|
if kwargs.get('pageable'):
|
||||||
warnings.warn("component param is deprecated for Grid(); "
|
warnings.warn("component param is deprecated for Grid(); "
|
||||||
"please use vue_tagname param instead",
|
"please use vue_tagname param instead",
|
||||||
|
@ -284,11 +289,6 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
self.searchable = searchable or {}
|
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.checkboxes = checkboxes
|
||||||
self.checked = checked
|
self.checked = checked
|
||||||
if self.checked is None:
|
if self.checked is None:
|
||||||
|
@ -328,9 +328,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component(self):
|
def component(self):
|
||||||
"""
|
""" """
|
||||||
DEPRECATED - use :attr:`vue_tagname` instead.
|
|
||||||
"""
|
|
||||||
warnings.warn("Grid.component is deprecated; "
|
warnings.warn("Grid.component is deprecated; "
|
||||||
"please use vue_tagname instead",
|
"please use vue_tagname instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
|
@ -338,20 +336,66 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component_studly(self):
|
def component_studly(self):
|
||||||
"""
|
""" """
|
||||||
DEPRECATED - use :attr:`vue_component` instead.
|
|
||||||
"""
|
|
||||||
warnings.warn("Grid.component_studly is deprecated; "
|
warnings.warn("Grid.component_studly is deprecated; "
|
||||||
"please use vue_component instead",
|
"please use vue_component instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
return self.vue_component
|
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):
|
def get_pageable(self):
|
||||||
""" """
|
""" """
|
||||||
|
warnings.warn("Grid.pageable is deprecated; "
|
||||||
|
"please use Grid.paginated instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
return self.paginated
|
return self.paginated
|
||||||
|
|
||||||
def set_pageable(self, value):
|
def set_pageable(self, value):
|
||||||
""" """
|
""" """
|
||||||
|
warnings.warn("Grid.pageable is deprecated; "
|
||||||
|
"please use Grid.paginated instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
self.paginated = value
|
self.paginated = value
|
||||||
|
|
||||||
pageable = property(get_pageable, set_pageable)
|
pageable = property(get_pageable, set_pageable)
|
||||||
|
@ -405,18 +449,30 @@ class Grid(WuttaGrid):
|
||||||
self.joiners[key] = joiner
|
self.joiners[key] = joiner
|
||||||
|
|
||||||
def set_sorter(self, key, *args, **kwargs):
|
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:
|
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)
|
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):
|
def set_filter(self, key, *args, **kwargs):
|
||||||
if len(args) == 1 and args[0] is None:
|
if len(args) == 1 and args[0] is None:
|
||||||
self.remove_filter(key)
|
self.remove_filter(key)
|
||||||
|
@ -731,53 +787,12 @@ class Grid(WuttaGrid):
|
||||||
if filtr.active:
|
if filtr.active:
|
||||||
yield filtr
|
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):
|
def make_simple_sorter(self, key, foldcase=False):
|
||||||
"""
|
""" """
|
||||||
Returns a function suitable for a sort map callable, with typical logic
|
warnings.warn("Grid.make_simple_sorter() is deprecated; "
|
||||||
built in for sorting a data set comprised of dicts, on the given key.
|
"please use Grid.make_sorter() instead",
|
||||||
"""
|
DeprecationWarning, stacklevel=2)
|
||||||
if foldcase:
|
return self.make_sorter(key, foldcase=foldcase)
|
||||||
keyfunc = lambda v: v[key].lower()
|
|
||||||
else:
|
|
||||||
keyfunc = lambda v: v[key]
|
|
||||||
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
|
|
||||||
|
|
||||||
def get_pagesize_options(self, default=None):
|
def get_pagesize_options(self, default=None):
|
||||||
""" """
|
""" """
|
||||||
|
@ -849,10 +864,17 @@ class Grid(WuttaGrid):
|
||||||
# initial default settings
|
# initial default settings
|
||||||
settings = {}
|
settings = {}
|
||||||
if self.sortable:
|
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.length'] = 1
|
||||||
settings['sorters.1.key'] = self.default_sortkey
|
settings['sorters.1.key'] = sortinfo.sortkey
|
||||||
settings['sorters.1.dir'] = self.default_sortdir
|
settings['sorters.1.dir'] = sortinfo.sortdir
|
||||||
else:
|
else:
|
||||||
settings['sorters.length'] = 0
|
settings['sorters.length'] = 0
|
||||||
if self.paginated:
|
if self.paginated:
|
||||||
|
@ -927,11 +949,12 @@ class Grid(WuttaGrid):
|
||||||
filtr.verb = settings['filter.{}.verb'.format(filtr.key)]
|
filtr.verb = settings['filter.{}.verb'.format(filtr.key)]
|
||||||
filtr.value = settings['filter.{}.value'.format(filtr.key)]
|
filtr.value = settings['filter.{}.value'.format(filtr.key)]
|
||||||
if self.sortable:
|
if self.sortable:
|
||||||
|
# and self.sort_on_backend:
|
||||||
self.active_sorters = []
|
self.active_sorters = []
|
||||||
for i in range(1, settings['sorters.length'] + 1):
|
for i in range(1, settings['sorters.length'] + 1):
|
||||||
self.active_sorters.append({
|
self.active_sorters.append({
|
||||||
'field': settings[f'sorters.{i}.key'],
|
'key': settings[f'sorters.{i}.key'],
|
||||||
'order': settings[f'sorters.{i}.dir'],
|
'dir': settings[f'sorters.{i}.dir'],
|
||||||
})
|
})
|
||||||
if self.paginated:
|
if self.paginated:
|
||||||
self.pagesize = settings['pagesize']
|
self.pagesize = settings['pagesize']
|
||||||
|
@ -1321,21 +1344,24 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def sort_data(self, data):
|
def sort_data(self, data, sorters=None):
|
||||||
"""
|
""" """
|
||||||
Sort the given query according to current settings, and return the result.
|
if sorters is None:
|
||||||
"""
|
sorters = self.active_sorters
|
||||||
# bail if no sort settings
|
if not sorters:
|
||||||
if not self.active_sorters:
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# TODO: is there a better way to check for SA sorting?
|
# sqlalchemy queries require special handling, in case of
|
||||||
if self.model_class:
|
# multi-column sorting
|
||||||
|
if isinstance(data, orm.Query):
|
||||||
|
|
||||||
# collect actual column sorters for order_by clause
|
# collect actual column sorters for order_by clause
|
||||||
sorters = []
|
query_sorters = []
|
||||||
for sorter in self.active_sorters:
|
for sorter in sorters:
|
||||||
sortkey = sorter['field']
|
sortkey = sorter['key']
|
||||||
|
sortdir = sorter['dir']
|
||||||
|
|
||||||
|
# cannot sort unless we have a sorter callable
|
||||||
sortfunc = self.sorters.get(sortkey)
|
sortfunc = self.sorters.get(sortkey)
|
||||||
if not sortfunc:
|
if not sortfunc:
|
||||||
log.warning("unknown sorter: %s", sorter)
|
log.warning("unknown sorter: %s", sorter)
|
||||||
|
@ -1347,34 +1373,36 @@ class Grid(WuttaGrid):
|
||||||
self.joined.add(sortkey)
|
self.joined.add(sortkey)
|
||||||
|
|
||||||
# add column/dir to collection
|
# add column/dir to collection
|
||||||
sortdir = sorter['order']
|
query_sorters.append(getattr(sortfunc._column, sortdir)())
|
||||||
sorters.append(getattr(sortfunc._column, sortdir)())
|
|
||||||
|
|
||||||
# apply sorting to query
|
# apply sorting to query
|
||||||
if sorters:
|
if query_sorters:
|
||||||
data = data.order_by(*sorters)
|
data = data.order_by(*query_sorters)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
else:
|
# manual sorting; only one column allowed
|
||||||
# not a SQLAlchemy grid, custom sorter
|
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']
|
# cannot sort unless we have a sorter callable
|
||||||
sortdir = self.active_sorters[0]['order'] or 'asc'
|
sortfunc = self.sorters.get(sortkey)
|
||||||
|
if not sortfunc:
|
||||||
|
return data
|
||||||
|
|
||||||
# Cannot sort unless we have a sort function.
|
# apply joins needed for this sorter
|
||||||
sortfunc = self.sorters.get(sortkey)
|
# TODO: is this actually relevant for manual sort?
|
||||||
if not sortfunc:
|
if sortkey in self.joiners and sortkey not in self.joined:
|
||||||
return data
|
data = self.joiners[sortkey](data)
|
||||||
|
self.joined.add(sortkey)
|
||||||
|
|
||||||
# apply joins needed for this sorter
|
# invoke the sorter
|
||||||
if sortkey in self.joiners and sortkey not in self.joined:
|
return sortfunc(data, sortdir)
|
||||||
data = self.joiners[sortkey](data)
|
|
||||||
self.joined.add(sortkey)
|
|
||||||
|
|
||||||
return sortfunc(data, sortdir)
|
|
||||||
|
|
||||||
def paginate_data(self, data):
|
def paginate_data(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -1671,7 +1699,7 @@ class Grid(WuttaGrid):
|
||||||
columns.append({
|
columns.append({
|
||||||
'field': name,
|
'field': name,
|
||||||
'label': self.get_label(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,
|
'visible': name not in self.invisible,
|
||||||
})
|
})
|
||||||
return columns
|
return columns
|
||||||
|
|
|
@ -81,7 +81,11 @@
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if getattr(grid, 'sortable', False):
|
## sorting
|
||||||
|
% if grid.sortable:
|
||||||
|
## nb. buefy only supports *one* default sorter
|
||||||
|
:default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null"
|
||||||
|
|
||||||
backend-sorting
|
backend-sorting
|
||||||
@sort="onSort"
|
@sort="onSort"
|
||||||
@sorting-priority-removed="sortingPriorityRemoved"
|
@sorting-priority-removed="sortingPriorityRemoved"
|
||||||
|
@ -93,8 +97,6 @@
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## https://github.com/buefy/buefy/issues/2584
|
||||||
:sort-multiple="allowMultiSort"
|
:sort-multiple="allowMultiSort"
|
||||||
|
|
||||||
## nb. specify default sort only if single-column
|
|
||||||
:default-sort="backendSorters.length == 1 ? [backendSorters[0].field, backendSorters[0].order] : null"
|
|
||||||
|
|
||||||
## nb. otherwise there may be default multi-column sort
|
## nb. otherwise there may be default multi-column sort
|
||||||
:sort-multiple-data="sortingPriority"
|
:sort-multiple-data="sortingPriority"
|
||||||
|
@ -272,7 +274,9 @@
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if getattr(grid, 'sortable', False):
|
## sorting
|
||||||
|
% if grid.sortable:
|
||||||
|
sorters: ${json.dumps(grid.active_sorters)|n},
|
||||||
|
|
||||||
## TODO: there is a bug (?) which prevents the arrow from
|
## TODO: there is a bug (?) which prevents the arrow from
|
||||||
## displaying for simple default single-column sort. so to
|
## displaying for simple default single-column sort. so to
|
||||||
|
@ -281,10 +285,7 @@
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## https://github.com/buefy/buefy/issues/2584
|
||||||
allowMultiSort: false,
|
allowMultiSort: false,
|
||||||
|
|
||||||
## nb. this contains all truly active sorters
|
## nb. this will only contain multi-column sorters,
|
||||||
backendSorters: ${json.dumps(grid.active_sorters)|n},
|
|
||||||
|
|
||||||
## nb. whereas this will only contain multi-column sorters,
|
|
||||||
## but will be *empty* for single-column sorting
|
## but will be *empty* for single-column sorting
|
||||||
% if len(grid.active_sorters) > 1:
|
% if len(grid.active_sorters) > 1:
|
||||||
sortingPriority: ${json.dumps(grid.active_sorters)|n},
|
sortingPriority: ${json.dumps(grid.active_sorters)|n},
|
||||||
|
@ -474,17 +475,18 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
getBasicParams() {
|
getBasicParams() {
|
||||||
let params = {}
|
const params = {
|
||||||
% if getattr(grid, 'sortable', False):
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
for (let i = 1; i <= this.backendSorters.length; i++) {
|
pagesize: this.perPage,
|
||||||
params['sort'+i+'key'] = this.backendSorters[i-1].field
|
page: this.currentPage,
|
||||||
params['sort'+i+'dir'] = this.backendSorters[i-1].order
|
% endif
|
||||||
|
}
|
||||||
|
% if grid.sortable and grid.sort_on_backend:
|
||||||
|
for (let i = 1; i <= this.sorters.length; i++) {
|
||||||
|
params['sort'+i+'key'] = this.sorters[i-1].key
|
||||||
|
params['sort'+i+'dir'] = this.sorters[i-1].dir
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
% if grid.paginated:
|
|
||||||
params.pagesize = this.perPage
|
|
||||||
params.page = this.currentPage
|
|
||||||
% endif
|
|
||||||
return params
|
return params
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -526,15 +528,15 @@
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => {
|
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => {
|
||||||
if (!response.data.error) {
|
if (!response.data.error) {
|
||||||
${grid.vue_component}CurrentData = response.data.data.data
|
${grid.vue_component}CurrentData = response.data.data
|
||||||
this.data = ${grid.vue_component}CurrentData
|
this.data = ${grid.vue_component}CurrentData
|
||||||
% if grid.paginated and grid.paginate_on_backend:
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
this.pagerStats = response.data.pager_stats
|
this.pagerStats = response.data.pager_stats
|
||||||
% endif
|
% endif
|
||||||
this.rowStatusMap = response.data.data.row_status_map
|
this.rowStatusMap = response.data.row_status_map || {}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.savingDefaults = false
|
this.savingDefaults = false
|
||||||
this.checkedRows = this.locateCheckedRows(response.data.data.checked_rows)
|
this.checkedRows = this.locateCheckedRows(response.data.checked_rows || [])
|
||||||
if (success) {
|
if (success) {
|
||||||
success()
|
success()
|
||||||
}
|
}
|
||||||
|
@ -597,26 +599,26 @@
|
||||||
|
|
||||||
onSort(field, order, event) {
|
onSort(field, order, event) {
|
||||||
|
|
||||||
// nb. buefy passes field name, oruga passes object
|
## nb. buefy passes field name; oruga passes field object
|
||||||
if (field.field) {
|
% if request.use_oruga:
|
||||||
field = field.field
|
field = field.field
|
||||||
}
|
% endif
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
|
|
||||||
// engage or enhance multi-column sorting
|
// engage or enhance multi-column sorting
|
||||||
let sorter = this.backendSorters.filter(i => i.field === field)[0]
|
const sorter = this.sorters.filter(s => s.key === field)[0]
|
||||||
if (sorter) {
|
if (sorter) {
|
||||||
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
|
sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc'
|
||||||
} else {
|
} else {
|
||||||
this.backendSorters.push({field, order})
|
this.sorters.push({key: field, dir: order})
|
||||||
}
|
}
|
||||||
this.sortingPriority = this.backendSorters
|
this.sortingPriority = this.sorters
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// sort by single column only
|
// sort by single column only
|
||||||
this.backendSorters = [{field, order}]
|
this.sorters = [{key: field, dir: order}]
|
||||||
this.sortingPriority = []
|
this.sortingPriority = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,12 +631,11 @@
|
||||||
sortingPriorityRemoved(field) {
|
sortingPriorityRemoved(field) {
|
||||||
|
|
||||||
// prune field from active sorters
|
// prune field from active sorters
|
||||||
this.backendSorters = this.backendSorters.filter(
|
this.sorters = this.sorters.filter(s => s.key !== field)
|
||||||
(sorter) => sorter.field !== field)
|
|
||||||
|
|
||||||
// nb. must keep active sorter list "as-is" even if
|
// nb. must keep active sorter list "as-is" even if
|
||||||
// there is only one sorter; buefy seems to expect it
|
// there is only one sorter; buefy seems to expect it
|
||||||
this.sortingPriority = this.backendSorters
|
this.sortingPriority = this.sorters
|
||||||
|
|
||||||
this.loadAsyncData()
|
this.loadAsyncData()
|
||||||
},
|
},
|
||||||
|
|
|
@ -345,8 +345,8 @@ class MasterView(View):
|
||||||
self.first_visible_grid_index = grid.pager.first_item
|
self.first_visible_grid_index = grid.pager.first_item
|
||||||
|
|
||||||
# return grid data only, if partial page was requested
|
# return grid data only, if partial page was requested
|
||||||
if self.request.params.get('partial'):
|
if self.request.GET.get('partial'):
|
||||||
context = {'data': grid.get_table_data()}
|
context = grid.get_table_data()
|
||||||
if grid.paginated and grid.paginate_on_backend:
|
if grid.paginated and grid.paginate_on_backend:
|
||||||
context['pager_stats'] = grid.get_vue_pager_stats()
|
context['pager_stats'] = grid.get_vue_pager_stats()
|
||||||
return self.json_response(context)
|
return self.json_response(context)
|
||||||
|
@ -2565,11 +2565,12 @@ class MasterView(View):
|
||||||
so if you like you can return a different help URL depending on which
|
so if you like you can return a different help URL depending on which
|
||||||
type of CRUD view is in effect, etc.
|
type of CRUD view is in effect, etc.
|
||||||
"""
|
"""
|
||||||
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
|
session = Session()
|
||||||
model = self.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
info = session.query(model.TailbonePageHelp)\
|
||||||
info = Session.query(model.TailbonePageHelp)\
|
|
||||||
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
||||||
.first()
|
.first()
|
||||||
if info and info.help_url:
|
if info and info.help_url:
|
||||||
|
@ -2587,11 +2588,12 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
Return the markdown help text for current page, if defined.
|
Return the markdown help text for current page, if defined.
|
||||||
"""
|
"""
|
||||||
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
|
session = Session()
|
||||||
model = self.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
info = session.query(model.TailbonePageHelp)\
|
||||||
info = Session.query(model.TailbonePageHelp)\
|
|
||||||
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
||||||
.first()
|
.first()
|
||||||
if info and info.markdown_text:
|
if info and info.markdown_text:
|
||||||
|
@ -2608,6 +2610,8 @@ class MasterView(View):
|
||||||
if not self.can_edit_help():
|
if not self.can_edit_help():
|
||||||
raise self.forbidden()
|
raise self.forbidden()
|
||||||
|
|
||||||
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
|
session = Session()
|
||||||
model = self.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
@ -2625,13 +2629,12 @@ class MasterView(View):
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
return {'error': "Form did not validate"}
|
return {'error': "Form did not validate"}
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
info = session.query(model.TailbonePageHelp)\
|
||||||
info = Session.query(model.TailbonePageHelp)\
|
|
||||||
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
|
||||||
.first()
|
.first()
|
||||||
if not info:
|
if not info:
|
||||||
info = model.TailbonePageHelp(route_prefix=route_prefix)
|
info = model.TailbonePageHelp(route_prefix=route_prefix)
|
||||||
Session.add(info)
|
session.add(info)
|
||||||
|
|
||||||
info.help_url = form.validated['help_url']
|
info.help_url = form.validated['help_url']
|
||||||
info.markdown_text = form.validated['markdown_text']
|
info.markdown_text = form.validated['markdown_text']
|
||||||
|
@ -2641,6 +2644,8 @@ class MasterView(View):
|
||||||
if not self.can_edit_help():
|
if not self.can_edit_help():
|
||||||
raise self.forbidden()
|
raise self.forbidden()
|
||||||
|
|
||||||
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
|
session = Session()
|
||||||
model = self.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
@ -2657,15 +2662,14 @@ class MasterView(View):
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
return {'error': "Form did not validate"}
|
return {'error': "Form did not validate"}
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
info = session.query(model.TailboneFieldInfo)\
|
||||||
info = Session.query(model.TailboneFieldInfo)\
|
|
||||||
.filter(model.TailboneFieldInfo.route_prefix == route_prefix)\
|
.filter(model.TailboneFieldInfo.route_prefix == route_prefix)\
|
||||||
.filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\
|
.filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\
|
||||||
.first()
|
.first()
|
||||||
if not info:
|
if not info:
|
||||||
info = model.TailboneFieldInfo(route_prefix=route_prefix,
|
info = model.TailboneFieldInfo(route_prefix=route_prefix,
|
||||||
field_name=form.validated['field_name'])
|
field_name=form.validated['field_name'])
|
||||||
Session.add(info)
|
session.add(info)
|
||||||
|
|
||||||
info.markdown_text = form.validated['markdown_text']
|
info.markdown_text = form.validated['markdown_text']
|
||||||
return {'ok': True}
|
return {'ok': True}
|
||||||
|
|
|
@ -44,6 +44,7 @@ class PersonView(wutta.PersonView):
|
||||||
"""
|
"""
|
||||||
model_class = Person
|
model_class = Person
|
||||||
Session = Session
|
Session = Session
|
||||||
|
sort_defaults = 'display_name'
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'display_name': "Full Name",
|
'display_name': "Full Name",
|
||||||
|
@ -73,13 +74,6 @@ class PersonView(wutta.PersonView):
|
||||||
# CRUD methods
|
# CRUD methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def get_query(self, session=None):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
session = session or self.Session()
|
|
||||||
return session.query(model.Person)\
|
|
||||||
.order_by(model.Person.display_name)
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
""" """
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from tailbone.grids import core as mod
|
from tailbone.grids import core as mod
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
@ -27,6 +29,16 @@ class TestGrid(WebTestCase):
|
||||||
grid = self.make_grid(component='blarg')
|
grid = self.make_grid(component='blarg')
|
||||||
self.assertEqual(grid.vue_tagname, 'blarg')
|
self.assertEqual(grid.vue_tagname, 'blarg')
|
||||||
|
|
||||||
|
# default_sortkey, default_sortdir
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
|
grid = self.make_grid(default_sortkey='name')
|
||||||
|
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])
|
||||||
|
grid = self.make_grid(default_sortdir='desc')
|
||||||
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
|
grid = self.make_grid(default_sortkey='name', default_sortdir='desc')
|
||||||
|
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])
|
||||||
|
|
||||||
# pageable
|
# pageable
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
self.assertFalse(grid.paginated)
|
self.assertFalse(grid.paginated)
|
||||||
|
@ -159,6 +171,27 @@ class TestGrid(WebTestCase):
|
||||||
grid.set_action_urls(setting, setting, 0)
|
grid.set_action_urls(setting, setting, 0)
|
||||||
self.assertEqual(setting['_action_url_view'], '/blarg')
|
self.assertEqual(setting['_action_url_view'], '/blarg')
|
||||||
|
|
||||||
|
def test_default_sortkey(self):
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
|
self.assertIsNone(grid.default_sortkey)
|
||||||
|
grid.default_sortkey = 'name'
|
||||||
|
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')])
|
||||||
|
self.assertEqual(grid.default_sortkey, 'name')
|
||||||
|
grid.default_sortkey = 'value'
|
||||||
|
self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')])
|
||||||
|
self.assertEqual(grid.default_sortkey, 'value')
|
||||||
|
|
||||||
|
def test_default_sortdir(self):
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
|
self.assertIsNone(grid.default_sortdir)
|
||||||
|
self.assertRaises(ValueError, setattr, grid, 'default_sortdir', 'asc')
|
||||||
|
grid.sort_defaults = [mod.SortInfo('name', 'asc')]
|
||||||
|
grid.default_sortdir = 'desc'
|
||||||
|
self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')])
|
||||||
|
self.assertEqual(grid.default_sortdir, 'desc')
|
||||||
|
|
||||||
def test_pageable(self):
|
def test_pageable(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
self.assertFalse(grid.paginated)
|
self.assertFalse(grid.paginated)
|
||||||
|
@ -219,6 +252,212 @@ class TestGrid(WebTestCase):
|
||||||
size = grid.get_pagesize()
|
size = grid.get_pagesize()
|
||||||
self.assertEqual(size, 15)
|
self.assertEqual(size, 15)
|
||||||
|
|
||||||
|
def test_set_sorter(self):
|
||||||
|
model = self.app.model
|
||||||
|
grid = self.make_grid(model_class=model.Setting,
|
||||||
|
sortable=True, sort_on_backend=True)
|
||||||
|
|
||||||
|
# passing None will remove sorter
|
||||||
|
self.assertIn('name', grid.sorters)
|
||||||
|
grid.set_sorter('name', None)
|
||||||
|
self.assertNotIn('name', grid.sorters)
|
||||||
|
|
||||||
|
# can recreate sorter with just column name
|
||||||
|
grid.set_sorter('name')
|
||||||
|
self.assertIn('name', grid.sorters)
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
self.assertNotIn('name', grid.sorters)
|
||||||
|
grid.set_sorter('name', 'name')
|
||||||
|
self.assertIn('name', grid.sorters)
|
||||||
|
|
||||||
|
# can recreate sorter with model property
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
self.assertNotIn('name', grid.sorters)
|
||||||
|
grid.set_sorter('name', model.Setting.name)
|
||||||
|
self.assertIn('name', grid.sorters)
|
||||||
|
|
||||||
|
# extra kwargs are ignored
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
self.assertNotIn('name', grid.sorters)
|
||||||
|
grid.set_sorter('name', model.Setting.name, foo='bar')
|
||||||
|
self.assertIn('name', grid.sorters)
|
||||||
|
|
||||||
|
# passing multiple args will invoke make_filter() directly
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
self.assertNotIn('name', grid.sorters)
|
||||||
|
with patch.object(grid, 'make_sorter') as make_sorter:
|
||||||
|
make_sorter.return_value = 42
|
||||||
|
grid.set_sorter('name', 'foo', 'bar')
|
||||||
|
make_sorter.assert_called_once_with('foo', 'bar')
|
||||||
|
self.assertEqual(grid.sorters['name'], 42)
|
||||||
|
|
||||||
|
def test_make_simple_sorter(self):
|
||||||
|
model = self.app.model
|
||||||
|
grid = self.make_grid(model_class=model.Setting,
|
||||||
|
sortable=True, sort_on_backend=True)
|
||||||
|
|
||||||
|
# delegates to grid.make_sorter()
|
||||||
|
with patch.object(grid, 'make_sorter') as make_sorter:
|
||||||
|
make_sorter.return_value = 42
|
||||||
|
sorter = grid.make_simple_sorter('name', foldcase=True)
|
||||||
|
make_sorter.assert_called_once_with('name', foldcase=True)
|
||||||
|
self.assertEqual(sorter, 42)
|
||||||
|
|
||||||
|
def test_load_settings(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# nb. first use a paging grid
|
||||||
|
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True,
|
||||||
|
pagesize=20, page=1)
|
||||||
|
|
||||||
|
# settings are loaded, applied, saved
|
||||||
|
self.assertEqual(grid.page, 1)
|
||||||
|
self.assertNotIn('grid.foo.page', self.request.session)
|
||||||
|
self.request.GET = {'pagesize': '10', 'page': '2'}
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertEqual(grid.page, 2)
|
||||||
|
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
||||||
|
|
||||||
|
# can skip the saving step
|
||||||
|
self.request.GET = {'pagesize': '10', 'page': '3'}
|
||||||
|
grid.load_settings(store=False)
|
||||||
|
self.assertEqual(grid.page, 3)
|
||||||
|
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
||||||
|
|
||||||
|
# no error for non-paginated grid
|
||||||
|
grid = self.make_grid(key='foo', paginated=False)
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertFalse(grid.paginated)
|
||||||
|
|
||||||
|
# nb. next use a sorting grid
|
||||||
|
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||||
|
sortable=True, sort_on_backend=True)
|
||||||
|
|
||||||
|
# settings are loaded, applied, saved
|
||||||
|
self.assertEqual(grid.sort_defaults, [])
|
||||||
|
self.assertFalse(hasattr(grid, 'active_sorters'))
|
||||||
|
self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'}
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
|
||||||
|
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'], 'desc')
|
||||||
|
|
||||||
|
# can skip the saving step
|
||||||
|
self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'}
|
||||||
|
grid.load_settings(store=False)
|
||||||
|
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
|
||||||
|
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'], 'desc')
|
||||||
|
|
||||||
|
# no error for non-sortable grid
|
||||||
|
grid = self.make_grid(key='foo', sortable=False)
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertFalse(grid.sortable)
|
||||||
|
|
||||||
|
# with sort defaults
|
||||||
|
grid = self.make_grid(model_class=model.Setting, sortable=True,
|
||||||
|
sort_on_backend=True, sort_defaults='name')
|
||||||
|
self.assertFalse(hasattr(grid, 'active_sorters'))
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
|
||||||
|
|
||||||
|
# with multi-column sort defaults
|
||||||
|
grid = self.make_grid(model_class=model.Setting, sortable=True,
|
||||||
|
sort_on_backend=True)
|
||||||
|
grid.sort_defaults = [
|
||||||
|
mod.SortInfo('name', 'asc'),
|
||||||
|
mod.SortInfo('value', 'desc'),
|
||||||
|
]
|
||||||
|
self.assertFalse(hasattr(grid, 'active_sorters'))
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}])
|
||||||
|
|
||||||
|
# load settings from session when nothing is in request
|
||||||
|
self.request.GET = {}
|
||||||
|
self.request.session.invalidate()
|
||||||
|
self.assertNotIn('grid.settings.sorters.length', self.request.session)
|
||||||
|
self.request.session['grid.settings.sorters.length'] = 1
|
||||||
|
self.request.session['grid.settings.sorters.1.key'] = 'name'
|
||||||
|
self.request.session['grid.settings.sorters.1.dir'] = 'desc'
|
||||||
|
grid = self.make_grid(key='settings', model_class=model.Setting,
|
||||||
|
sortable=True, sort_on_backend=True,
|
||||||
|
paginated=True, paginate_on_backend=True)
|
||||||
|
self.assertFalse(hasattr(grid, 'active_sorters'))
|
||||||
|
grid.load_settings()
|
||||||
|
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
|
||||||
|
|
||||||
|
def test_sort_data(self):
|
||||||
|
model = self.app.model
|
||||||
|
sample_data = [
|
||||||
|
{'name': 'foo1', 'value': 'ONE'},
|
||||||
|
{'name': 'foo2', 'value': 'two'},
|
||||||
|
{'name': 'foo3', 'value': 'three'},
|
||||||
|
{'name': 'foo4', 'value': 'four'},
|
||||||
|
{'name': 'foo5', 'value': 'five'},
|
||||||
|
{'name': 'foo6', 'value': 'six'},
|
||||||
|
{'name': 'foo7', 'value': 'seven'},
|
||||||
|
{'name': 'foo8', 'value': 'eight'},
|
||||||
|
{'name': 'foo9', 'value': 'nine'},
|
||||||
|
]
|
||||||
|
for setting in sample_data:
|
||||||
|
self.app.save_setting(self.session, setting['name'], setting['value'])
|
||||||
|
self.session.commit()
|
||||||
|
sample_query = self.session.query(model.Setting)
|
||||||
|
|
||||||
|
grid = self.make_grid(model_class=model.Setting,
|
||||||
|
sortable=True, sort_on_backend=True,
|
||||||
|
sort_defaults=('name', 'desc'))
|
||||||
|
grid.load_settings()
|
||||||
|
|
||||||
|
# can sort a simple list of data
|
||||||
|
sorted_data = grid.sort_data(sample_data)
|
||||||
|
self.assertIsInstance(sorted_data, list)
|
||||||
|
self.assertEqual(len(sorted_data), 9)
|
||||||
|
self.assertEqual(sorted_data[0]['name'], 'foo9')
|
||||||
|
self.assertEqual(sorted_data[-1]['name'], 'foo1')
|
||||||
|
|
||||||
|
# can also sort a data query
|
||||||
|
sorted_query = grid.sort_data(sample_query)
|
||||||
|
self.assertIsInstance(sorted_query, orm.Query)
|
||||||
|
sorted_data = sorted_query.all()
|
||||||
|
self.assertEqual(len(sorted_data), 9)
|
||||||
|
self.assertEqual(sorted_data[0]['name'], 'foo9')
|
||||||
|
self.assertEqual(sorted_data[-1]['name'], 'foo1')
|
||||||
|
|
||||||
|
# cannot sort data if sorter missing in overrides
|
||||||
|
sorted_data = grid.sort_data(sample_data, sorters=[])
|
||||||
|
# nb. sorted data is in same order as original sample (not sorted)
|
||||||
|
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
||||||
|
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
||||||
|
|
||||||
|
# error if mult-column sort attempted
|
||||||
|
self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[
|
||||||
|
{'key': 'name', 'dir': 'desc'},
|
||||||
|
{'key': 'value', 'dir': 'asc'},
|
||||||
|
])
|
||||||
|
|
||||||
|
# cannot sort data if sortfunc is missing for column
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
sorted_data = grid.sort_data(sample_data)
|
||||||
|
# nb. sorted data is in same order as original sample (not sorted)
|
||||||
|
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
||||||
|
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
||||||
|
|
||||||
|
# cannot sort data if sortfunc is missing for column
|
||||||
|
grid.remove_sorter('name')
|
||||||
|
# nb. attempting multi-column sort, but only one sorter exists
|
||||||
|
self.assertEqual(list(grid.sorters), ['value'])
|
||||||
|
grid.active_sorters = [{'key': 'name', 'dir': 'asc'},
|
||||||
|
{'key': 'value', 'dir': 'asc'}]
|
||||||
|
with patch.object(sample_query, 'order_by') as order_by:
|
||||||
|
order_by.return_value = 42
|
||||||
|
sorted_query = grid.sort_data(sample_query)
|
||||||
|
order_by.assert_called_once()
|
||||||
|
self.assertEqual(len(order_by.call_args.args), 1)
|
||||||
|
self.assertEqual(sorted_query, 42)
|
||||||
|
|
||||||
def test_render_vue_tag(self):
|
def test_render_vue_tag(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -249,11 +488,13 @@ class TestGrid(WebTestCase):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
grid = self.make_grid('settings', model_class=model.Setting)
|
grid = self.make_grid('settings', model_class=model.Setting, sortable=True)
|
||||||
columns = grid.get_vue_columns()
|
columns = grid.get_vue_columns()
|
||||||
self.assertEqual(len(columns), 2)
|
self.assertEqual(len(columns), 2)
|
||||||
self.assertEqual(columns[0]['field'], 'name')
|
self.assertEqual(columns[0]['field'], 'name')
|
||||||
|
self.assertTrue(columns[0]['sortable'])
|
||||||
self.assertEqual(columns[1]['field'], 'value')
|
self.assertEqual(columns[1]['field'], 'value')
|
||||||
|
self.assertTrue(columns[1]['sortable'])
|
||||||
|
|
||||||
def test_get_vue_data(self):
|
def test_get_vue_data(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from tailbone.views import master as mod
|
from tailbone.views import master as mod
|
||||||
from wuttaweb.grids import GridAction
|
from wuttaweb.grids import GridAction
|
||||||
|
@ -33,3 +33,34 @@ class TestMasterView(WebTestCase):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
action = view.make_action('view')
|
action = view.make_action('view')
|
||||||
self.assertIsInstance(action, GridAction)
|
self.assertIsInstance(action, GridAction)
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
self.pyramid_config.include('tailbone.views.common')
|
||||||
|
self.pyramid_config.include('tailbone.views.auth')
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# mimic view for /settings
|
||||||
|
with patch.object(mod, 'Session', return_value=self.session):
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting,
|
||||||
|
Session=MagicMock(return_value=self.session),
|
||||||
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
|
get_help_url=MagicMock(return_value=None)):
|
||||||
|
|
||||||
|
# basic
|
||||||
|
view = self.make_view()
|
||||||
|
response = view.index()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# then again with data, to include view action url
|
||||||
|
data = [{'name': 'foo', 'value': 'bar'}]
|
||||||
|
with patch.object(view, 'get_data', return_value=data):
|
||||||
|
response = view.index()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content_type, 'text/html')
|
||||||
|
|
||||||
|
# then once more as 'partial' - aka. data only
|
||||||
|
self.request.GET = {'partial': '1'}
|
||||||
|
response = view.index()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
|
Loading…
Reference in a new issue