feat: move multi-column grid sorting logic to wuttaweb

tailbone grid template still duplicates much for Vue, and will until
we can port the filters and anything else remaining..
This commit is contained in:
Lance Edgar 2024-08-18 19:22:04 -05:00
parent ec36df4a34
commit 290f8fd51e
5 changed files with 252 additions and 292 deletions

View file

@ -850,28 +850,23 @@ class Grid(WuttaGrid):
return self.get_pagesize()
def load_settings(self, store=True):
"""
Load current/effective settings for the grid, from the request query
string and/or session storage. If ``store`` is true, then once
settings have been fully read, they are stored in current session for
next time. Finally, various instance attributes of the grid and its
filters are updated in-place to reflect the settings; this is so code
needn't access the settings dict directly, but the more Pythonic
instance attributes.
"""
def load_settings(self, **kwargs):
""" """
if 'store' in kwargs:
warnings.warn("the 'store' param is deprecated for load_settings(); "
"please use the 'persist' param instead",
DeprecationWarning, stacklevel=2)
kwargs.setdefault('persist', kwargs.pop('store'))
persist = kwargs.get('persist', True)
# initial default settings
settings = {}
if self.sortable:
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]
# nb. as of writing neither Buefy nor Oruga support a
# multi-column *default* sort; so just use first sorter
sortinfo = self.sort_defaults[0]
settings['sorters.length'] = 1
settings['sorters.1.key'] = sortinfo.sortkey
settings['sorters.1.dir'] = sortinfo.sortdir
@ -900,16 +895,16 @@ class Grid(WuttaGrid):
elif self.filterable and self.request_has_settings('filter'):
self.update_filter_settings(settings, 'request')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
self.update_sort_settings(settings, src='request')
else:
self.update_sort_settings(settings, 'session')
self.update_sort_settings(settings, src='session')
self.update_page_settings(settings)
# If request has no filter settings but does have sort settings, grab
# those, then grab filter settings from session, then grab pager
# settings from request or session.
elif self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
self.update_sort_settings(settings, src='request')
self.update_filter_settings(settings, 'session')
self.update_page_settings(settings)
@ -921,26 +916,26 @@ class Grid(WuttaGrid):
elif self.request_has_settings('page'):
self.update_page_settings(settings)
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
self.update_sort_settings(settings, src='session')
# If request has no settings, grab all from session.
elif self.session_has_settings():
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
self.update_sort_settings(settings, src='session')
self.update_page_settings(settings)
# If no settings were found in request or session, don't store result.
else:
store = False
persist = False
# Maybe store settings for next time.
if store:
self.persist_settings(settings, 'session')
if persist:
self.persist_settings(settings, dest='session')
# 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, 'defaults')
self.persist_settings(settings, dest='defaults')
# update ourself to reflect settings
if self.filterable:
@ -1107,44 +1102,6 @@ class Grid(WuttaGrid):
return any([key.startswith(f'{prefix}.filter')
for key in self.request.session])
def get_setting(self, source, settings, key, normalize=lambda v: v, default=None):
"""
Get the effective value for a particular setting, preferring ``source``
but falling back to existing ``settings`` and finally the ``default``.
"""
if source not in ('request', 'session'):
raise ValueError("Invalid source identifier: {}".format(source))
# If source is query string, try that first.
if source == 'request':
value = self.request.GET.get(key)
if value is not None:
try:
value = normalize(value)
except ValueError:
pass
else:
return value
# Or, if source is session, try that first.
else:
value = self.request.session.get('grid.{}.{}'.format(self.key, key))
if value is not None:
return normalize(value)
# If source had nothing, try default/existing settings.
value = settings.get(key)
if value is not None:
try:
value = normalize(value)
except ValueError:
pass
else:
return value
# Okay then, default it is.
return default
def update_filter_settings(self, settings, source):
"""
Updates a settings dictionary according to filter settings data found
@ -1165,71 +1122,18 @@ class Grid(WuttaGrid):
# consider filter active if query string contains a value for it
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
settings['{}.verb'.format(prefix)] = self.get_setting(
source, settings, '{}.verb'.format(filtr.key), default='')
settings, f'{filtr.key}.verb', src='request', default='')
settings['{}.value'.format(prefix)] = self.get_setting(
source, settings, filtr.key, default='')
settings, filtr.key, src='request', default='')
else: # source = session
settings['{}.active'.format(prefix)] = self.get_setting(
source, settings, '{}.active'.format(prefix),
settings, f'{prefix}.active', src='session',
normalize=lambda v: str(v).lower() == 'true', default=False)
settings['{}.verb'.format(prefix)] = self.get_setting(
source, settings, '{}.verb'.format(prefix), default='')
settings, f'{prefix}.verb', src='session', default='')
settings['{}.value'.format(prefix)] = self.get_setting(
source, settings, '{}.value'.format(prefix), default='')
def update_sort_settings(self, settings, source):
"""
Updates a settings dictionary according to sort settings data found in
either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.sortable:
return
if source == 'request':
# TODO: remove this eventually, but some links in the wild
# may still include these params, so leave it for now
if 'sortkey' in self.request.GET:
settings['sorters.length'] = 1
settings['sorters.1.key'] = self.get_setting(source, settings, 'sortkey')
settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir')
else: # the future
i = 1
while True:
skey = f'sort{i}key'
if skey in self.request.GET:
settings[f'sorters.{i}.key'] = self.get_setting(source, settings, skey)
settings[f'sorters.{i}.dir'] = self.get_setting(source, settings, f'sort{i}dir')
else:
break
i += 1
settings['sorters.length'] = i - 1
else: # session
# TODO: definitely will remove this, but leave it for now
# so it doesn't monkey with current user sessions when
# next upgrade happens. so, remove after all are upgraded
sortkey = self.get_setting(source, settings, 'sortkey')
if sortkey:
settings['sorters.length'] = 1
settings['sorters.1.key'] = sortkey
settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir')
else: # the future
settings['sorters.length'] = self.get_setting(source, settings,
'sorters.length', int)
for i in range(1, settings['sorters.length'] + 1):
for key in ('key', 'dir'):
skey = f'sorters.{i}.{key}'
settings[skey] = self.get_setting(source, settings, skey)
settings, f'{prefix}.value', src='session', default='')
def update_page_settings(self, settings):
"""
@ -1264,18 +1168,19 @@ class Grid(WuttaGrid):
if page is not None:
settings['page'] = int(page)
def persist_settings(self, settings, to='session'):
"""
Persist the given settings in some way, as defined by ``func``.
"""
def persist_settings(self, settings, dest='session'):
""" """
if dest not in ('defaults', 'session'):
raise ValueError(f"invalid dest identifier: {dest}")
app = self.request.rattail_config.get_app()
model = app.model
def persist(key, value=lambda k: settings[k]):
if to == 'defaults':
def persist(key, value=lambda k: settings.get(k)):
if dest == 'defaults':
skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
app.save_setting(Session(), skey, value(key))
else: # to == session
else: # dest == session
skey = 'grid.{}.{}'.format(self.key, key)
self.request.session[skey] = value(key)
@ -1287,9 +1192,11 @@ class Grid(WuttaGrid):
if self.sortable:
# first clear existing settings for *sorting* only
# nb. this is because number of sort settings will vary
if to == 'defaults':
# first must clear all sort settings from dest. this is
# because number of sort settings will vary, so we delete
# all and then write all
if dest == 'defaults':
prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
query = Session.query(model.Setting)\
.filter(sa.or_(
@ -1303,7 +1210,9 @@ class Grid(WuttaGrid):
for setting in query.all():
Session.delete(setting)
Session.flush()
else: # session
# remove sort settings from user session
prefix = f'grid.{self.key}'
for key in list(self.request.session):
if key.startswith(f'{prefix}.sorters.'):
@ -1315,10 +1224,12 @@ class Grid(WuttaGrid):
self.request.session.pop(f'{prefix}.sortkey', None)
self.request.session.pop(f'{prefix}.sortdir', None)
persist('sorters.length')
for i in range(1, settings['sorters.length'] + 1):
persist(f'sorters.{i}.key')
persist(f'sorters.{i}.dir')
# now save sort settings to dest
if 'sorters.length' in settings:
persist('sorters.length')
for i in range(1, settings['sorters.length'] + 1):
persist(f'sorters.{i}.key')
persist(f'sorters.{i}.dir')
if self.paginated:
persist('pagesize')
@ -1351,58 +1262,32 @@ class Grid(WuttaGrid):
if not sorters:
return data
# sqlalchemy queries require special handling, in case of
# multi-column sorting
if isinstance(data, orm.Query):
# nb. when data is a query, we want to apply sorters in the
# requested order, so the final query has order_by() in the
# correct "as-is" sequence. however when data is a list we
# must do the opposite, applying in the reverse order, so the
# final list has the most "important" sort(s) applied last.
if not isinstance(data, orm.Query):
sorters = reversed(sorters)
# collect actual column sorters for order_by clause
query_sorters = []
for sorter in sorters:
sortkey = sorter['key']
sortdir = sorter['dir']
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)
continue
# cannot sort unless we have a sorter callable
sortfunc = self.sorters.get(sortkey)
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)
# 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)
# add column/dir to collection
query_sorters.append(getattr(sortfunc._column, sortdir)())
# invoke the sorter
data = sortfunc(data, sortdir)
# apply sorting to query
if query_sorters:
data = data.order_by(*query_sorters)
return data
# manual sorting; only one column allowed
if len(sorters) != 1:
raise NotImplementedError("mulit-column manual sorting not yet supported")
# our one and only active sorter
sorter = sorters[0]
sortkey = sorter['key']
sortdir = sorter['dir']
# cannot sort unless we have a sorter callable
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)
# invoke the sorter
return sortfunc(data, sortdir)
return data
def paginate_data(self, data):
"""

View file

@ -658,19 +658,19 @@
## TODO: is there a better way to check if viewing parent?
% if parent_instance is Undefined:
% if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${action_url('edit', instance)}"
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>
% endif
% if master.cloneable and master.has_perm('clone'):
<once-button tag="a" href="${action_url('clone', instance)}"
% if getattr(master, 'cloneable', False) and master.has_perm('clone'):
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
icon-left="object-ungroup"
text="Clone This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -679,7 +679,7 @@
% else:
## viewing row
% if instance_deletable and master.has_perm('delete_row'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -688,13 +688,13 @@
% endif
% elif master and master.editing:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}"
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -702,13 +702,13 @@
% endif
% elif master and master.deleting:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}"
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${action_url('edit', instance)}"
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>

View file

@ -83,26 +83,29 @@
## sorting
% if grid.sortable:
## nb. buefy only supports *one* default sorter
:default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null"
backend-sorting
@sort="onSort"
@sorting-priority-removed="sortingPriorityRemoved"
## TODO: there is a bug (?) which prevents the arrow from
## displaying for simple default single-column sort. so to
## work around that, we *disable* multi-sort until the
## component is mounted. seems to work for now..see also
## https://github.com/buefy/buefy/issues/2584
:sort-multiple="allowMultiSort"
## nb. otherwise there may be default multi-column sort
:sort-multiple-data="sortingPriority"
## user must ctrl-click column header to do multi-sort
sort-multiple-key="ctrlKey"
## nb. buefy/oruga only support *one* default sorter
:default-sort="sorters.length ? [sorters[0].field, sorters[0].order] : null"
% if grid.sort_on_backend:
backend-sorting
@sort="onSort"
% endif
% if grid.sort_multiple:
% if grid.sort_on_backend:
## TODO: there is a bug (?) which prevents the arrow
## from displaying for simple default single-column sort,
## when multi-column sort is allowed for the table. for
## now we work around that by waiting until mount to
## enable the multi-column support. see also
## https://github.com/buefy/buefy/issues/2584
:sort-multiple="allowMultiSort"
:sort-multiple-data="sortingPriority"
@sorting-priority-removed="sortingPriorityRemoved"
% else:
sort-multiple
% endif
## nb. user must ctrl-click column header for multi-sort
sort-multiple-key="ctrlKey"
% endif
% endif
% if getattr(grid, 'click_handlers', None):
@ -276,23 +279,24 @@
## sorting
% if grid.sortable:
sorters: ${json.dumps(grid.active_sorters)|n},
## TODO: there is a bug (?) which prevents the arrow from
## displaying for simple default single-column sort. so to
## work around that, we *disable* multi-sort until the
## component is mounted. seems to work for now..see also
## https://github.com/buefy/buefy/issues/2584
allowMultiSort: false,
## nb. this will only contain multi-column sorters,
## but will be *empty* for single-column sorting
% if len(grid.active_sorters) > 1:
sortingPriority: ${json.dumps(grid.active_sorters)|n},
% else:
sortingPriority: [],
sorters: ${json.dumps(grid.get_vue_active_sorters())|n},
% if grid.sort_multiple:
% if grid.sort_on_backend:
## TODO: there is a bug (?) which prevents the arrow
## from displaying for simple default single-column sort,
## when multi-column sort is allowed for the table. for
## now we work around that by waiting until mount to
## enable the multi-column support. see also
## https://github.com/buefy/buefy/issues/2584
allowMultiSort: false,
## nb. this should be empty when current sort is single-column
% if len(grid.active_sorters) > 1:
sortingPriority: ${json.dumps(grid.get_vue_active_sorters())|n},
% else:
sortingPriority: [],
% endif
% endif
% endif
% endif
## filterable: ${json.dumps(grid.filterable)|n},
@ -395,14 +399,19 @@
},
},
mounted() {
## TODO: there is a bug (?) which prevents the arrow from
## displaying for simple default single-column sort. so to
## work around that, we *disable* multi-sort until the
## component is mounted. seems to work for now..see also
## https://github.com/buefy/buefy/issues/2584
this.allowMultiSort = true
},
% if grid.sortable and grid.sort_multiple and grid.sort_on_backend:
## TODO: there is a bug (?) which prevents the arrow
## from displaying for simple default single-column sort,
## when multi-column sort is allowed for the table. for
## now we work around that by waiting until mount to
## enable the multi-column support. see also
## https://github.com/buefy/buefy/issues/2584
mounted() {
this.allowMultiSort = true
},
% endif
methods: {
@ -483,8 +492,8 @@
}
% 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
params['sort'+i+'key'] = this.sorters[i-1].field
params['sort'+i+'dir'] = this.sorters[i-1].order
}
% endif
return params
@ -597,48 +606,66 @@
})
},
onSort(field, order, event) {
% if grid.sortable and grid.sort_on_backend:
## nb. buefy passes field name; oruga passes field object
% if request.use_oruga:
field = field.field
% endif
onSort(field, order, event) {
if (event.ctrlKey) {
## nb. buefy passes field name; oruga passes field object
% if request.use_oruga:
field = field.field
% endif
// engage or enhance multi-column sorting
const sorter = this.sorters.filter(s => s.key === field)[0]
if (sorter) {
sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc'
} else {
this.sorters.push({key: field, dir: order})
}
this.sortingPriority = this.sorters
% if grid.sort_multiple:
} else {
// did user ctrl-click the column header?
if (event.ctrlKey) {
// toggle direction for existing, or add new sorter
const sorter = this.sorters.filter(s => s.field === field)[0]
if (sorter) {
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
} else {
this.sorters.push({field, order})
}
// apply multi-column sorting
this.sortingPriority = this.sorters
} else {
% endif
// sort by single column only
this.sorters = [{key: field, dir: order}]
this.sortingPriority = []
}
this.sorters = [{field, order}]
// always reset to first page when changing sort options
// TODO: i mean..right? would we ever not want that?
this.currentPage = 1
this.loadAsyncData()
},
% if grid.sort_multiple:
// multi-column sort not engaged
this.sortingPriority = []
}
% endif
sortingPriorityRemoved(field) {
// nb. always reset to first page when sorting changes
this.currentPage = 1
this.loadAsyncData()
},
// prune field from active sorters
this.sorters = this.sorters.filter(s => s.key !== field)
% if grid.sort_multiple:
// nb. must keep active sorter list "as-is" even if
// there is only one sorter; buefy seems to expect it
this.sortingPriority = this.sorters
sortingPriorityRemoved(field) {
this.loadAsyncData()
},
// prune from active sorters
this.sorters = this.sorters.filter(s => s.field !== field)
// nb. even though we might have just one sorter
// now, we are still technically in multi-sort mode
this.sortingPriority = this.sorters
this.loadAsyncData()
},
% endif
% endif
resetView() {
this.loading = true

View file

@ -341,7 +341,7 @@ class MasterView(View):
return self.redirect(self.request.current_route_url(**kw))
# Stash some grid stats, for possible use when generating URLs.
if grid.pageable and hasattr(grid, 'pager'):
if grid.paginated and hasattr(grid, 'pager'):
self.first_visible_grid_index = grid.pager.first_item
# return grid data only, if partial page was requested
@ -442,6 +442,7 @@ class MasterView(View):
'filterable': self.filterable,
'use_byte_string_filters': self.use_byte_string_filters,
'sortable': self.sortable,
'sort_multiple': not self.request.use_oruga,
'paginated': self.pageable,
'extra_row_class': self.grid_extra_class,
'url': lambda obj: self.get_action_url('view', obj),

View file

@ -388,14 +388,63 @@ class TestGrid(WebTestCase):
grid.load_settings()
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
def test_persist_settings(self):
model = self.app.model
# nb. start out with paginated-only grid
grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True)
# invalid dest
self.assertRaises(ValueError, grid.persist_settings, {}, dest='doesnotexist')
# nb. no error if empty settings, but it saves null values
grid.persist_settings({}, dest='session')
self.assertIsNone(self.request.session['grid.foo.page'])
# provided values are saved
grid.persist_settings({'pagesize': 15, 'page': 3}, dest='session')
self.assertEqual(self.request.session['grid.foo.page'], 3)
# nb. now switch to sortable-only grid
grid = self.make_grid(key='settings', model_class=model.Setting,
sortable=True, sort_on_backend=True)
# no error if empty settings; does not save values
grid.persist_settings({}, dest='session')
self.assertNotIn('grid.settings.sorters.length', self.request.session)
# provided values are saved
grid.persist_settings({'sorters.length': 2,
'sorters.1.key': 'name',
'sorters.1.dir': 'desc',
'sorters.2.key': 'value',
'sorters.2.dir': 'asc'},
dest='session')
self.assertEqual(self.request.session['grid.settings.sorters.length'], 2)
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc')
self.assertEqual(self.request.session['grid.settings.sorters.2.key'], 'value')
self.assertEqual(self.request.session['grid.settings.sorters.2.dir'], 'asc')
# old values removed when new are saved
grid.persist_settings({'sorters.length': 1,
'sorters.1.key': 'name',
'sorters.1.dir': 'desc'},
dest='session')
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')
self.assertNotIn('grid.settings.sorters.2.key', self.request.session)
self.assertNotIn('grid.settings.sorters.2.dir', self.request.session)
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': 'foo3', 'value': 'ggg'},
{'name': 'foo4', 'value': 'ggg'},
{'name': 'foo5', 'value': 'ggg'},
{'name': 'foo6', 'value': 'six'},
{'name': 'foo7', 'value': 'seven'},
{'name': 'foo8', 'value': 'eight'},
@ -432,32 +481,30 @@ class TestGrid(WebTestCase):
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'},
])
# multi-column sorting for list data
sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})
# multi-column sorting for query
sorted_query = grid.sort_data(sample_query, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'})
self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'})
self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'})
# cannot sort data if sortfunc is missing for column
grid.remove_sorter('name')
sorted_data = grid.sort_data(sample_data)
sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
{'key': 'name', 'dir': 'asc'}])
# 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):
model = self.app.model