From 8d6f4ad368499a620a050c0ed4f9e4f99af464bc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 19:23:15 -0500 Subject: [PATCH] feat: add multi-column sorting (frontend or backend) for grids --- src/wuttaweb/grids/base.py | 272 +++++++++++++----- .../templates/grids/vue_template.mako | 103 ++++++- src/wuttaweb/views/master.py | 1 + tests/grids/test_base.py | 105 +++++-- 4 files changed, 388 insertions(+), 93 deletions(-) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index e6d3217..e193109 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -131,7 +131,35 @@ class Grid: Boolean indicating whether *any* column sorting is allowed for the grid. Default is ``False``. - See also :attr:`sort_on_backend`. + See also :attr:`sort_multiple` and :attr:`sort_on_backend`. + + .. attribute:: sort_multiple + + Boolean indicating whether "multi-column" sorting is allowed. + Default is ``True``; if this is ``False`` then only one column + may be sorted at a time. + + Only relevant if :attr:`sortable` is true, but applies to both + frontend and backend sorting. + + .. warning:: + + This feature is limited by frontend JS capabilities, + regardless of :attr:`sort_on_backend` value (i.e. for both + frontend and backend sorting). + + In particular, if the app theme templates use Vue 2 + Buefy, + then multi-column sorting should work. + + But not so with Vue 3 + Oruga, *yet* - see also the `open + issue `_ + regarding that. For now this flag is simply ignored for + Vue 3 + Oruga templates. + + Additionally, even with Vue 2 + Buefy this flag can only + allow the user to *request* a multi-column sort. Whereas + the "default sort" in the Vue component can only ever be + single-column, regardless of :attr:`sort_defaults`. .. attribute:: sort_on_backend @@ -150,27 +178,64 @@ class Grid: Only relevant if both :attr:`sortable` and :attr:`sort_on_backend` are true. - See also :meth:`set_sorter()`. + See also :meth:`set_sorter()`, :attr:`sort_defaults` and + :attr:`active_sorters`. .. attribute:: sort_defaults List of options to be used for default sorting, until the user requests a different sorting method. - This list usually contains either zero or one elements. Each - element is a :class:`SortInfo` tuple. + This list usually contains either zero or one elements. (More + are allowed if :attr:`sort_multiple` is true, but see note + below.) Each list element is a :class:`SortInfo` tuple and + must correspond to an entry in :attr:`sorters`. Used with both frontend and backend sorting. - See also :meth:`set_sort_defaults()`. + See also :meth:`set_sort_defaults()` and + :attr:`active_sorters`. - .. note:: + .. warning:: - While the grid logic is meant to handle multi-column - sorting, that is not yet fully implemented. + While the grid logic is built to handle multi-column + sorting, this feature is limited by frontend JS + capabilities. - Therefore only the *first* element from this list is used - for the actual default sorting, regardless. + Even if ``sort_defaults`` contains multiple entries + (i.e. for multi-column sorting to be used "by default" for + the grid), only the *first* entry (i.e. single-column + sorting) will actually be used as the default for the Vue + component. + + See also :attr:`sort_multiple` for more details. + + .. attribute:: active_sorters + + List of sorters currently in effect for the grid; used by + :meth:`sort_data()`. + + Whereas :attr:`sorters` defines all "available" sorters, and + :attr:`sort_defaults` defines the "default" sorters, + ``active_sorters`` defines the "current/effective" sorters. + + This attribute is set by :meth:`load_settings()`; until that is + called it will not exist. + + This is conceptually a "subset" of :attr:`sorters` although a + different format is used here:: + + grid.active_sorters = [ + {'key': 'name', 'dir': 'asc'}, + {'key': 'id', 'dir': 'asc'}, + ] + + The above is for example only; there is usually no reason to + set this attribute directly. + + This list may contain multiple elements only if + :attr:`sort_multiple` is true. Otherewise it should always + have either zero or one element. .. attribute:: paginated @@ -232,6 +297,7 @@ class Grid: actions=[], linked_columns=[], sortable=False, + sort_multiple=True, sort_on_backend=True, sorters=None, sort_defaults=None, @@ -258,6 +324,10 @@ class Grid: # sorting self.sortable = sortable + self.sort_multiple = sort_multiple + if self.sort_multiple and self.request.use_oruga: + log.warning("grid.sort_multiple is not implemented for Oruga-based templates") + self.sort_multiple = False self.sort_on_backend = sort_on_backend if sorters is not None: self.sorters = sorters @@ -265,24 +335,7 @@ class Grid: self.sorters = self.make_backend_sorters() else: self.sorters = {} - if sort_defaults: - if isinstance(sort_defaults, str): - key = sort_defaults - sort_defaults = [SortInfo(key, 'asc')] - elif (isinstance(sort_defaults, tuple) - and len(sort_defaults) == 2 - and all([isinstance(el, str) for el in sort_defaults])): - sort_defaults = [SortInfo(*sort_defaults)] - else: - sort_defaults = [SortInfo(*info) for info in 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]] - self.sort_defaults = sort_defaults - else: - self.sort_defaults = [] + self.set_sort_defaults(sort_defaults or []) # paging self.paginated = paginated @@ -531,7 +584,7 @@ class Grid: return sorters - def make_sorter(self, columninfo, keyfunc=None, foldcase=False): + def make_sorter(self, columninfo, keyfunc=None, foldcase=True): """ Returns a function suitable for use as a backend sorter on the given column. @@ -549,7 +602,9 @@ class Grid: :param foldcase: If the sorter is manual (not SQLAlchemy), and the column data is of text type, this may be used to - automatically "fold case" for the sorting. + automatically "fold case" for the sorting. Defaults to + ``True`` since this behavior is presumably expected, but + may be disabled if needed. The term "model property" is a bit technical, an example should help to clarify:: @@ -699,20 +754,68 @@ class Grid: """ self.sorters.pop(key, None) - def set_sort_defaults(self, sortkey, sortdir='asc'): + def set_sort_defaults(self, *args): """ - Set the default sorting method for the grid. + Set the default sorting method for the grid. This sorting is + used unless/until the user requests a different sorting + method. - This sorting is used unless/until the user requests a - different sorting method. + ``args`` for this method are interpreted as follows: - :param sortkey: Name of the column by which to sort. + If 2 args are received, they should be for ``sortkey`` and + ``sortdir``; for instance:: - :param sortdir: Must be either ``'asc'`` or ``'desc'``. + grid.set_sort_defaults('name', 'asc') + + If just one 2-tuple arg is received, it is handled similarly:: + + grid.set_sort_defaults(('name', 'asc')) + + If just one string arg is received, the default ``sortdir`` is + assumed:: + + grid.set_sort_defaults('name') # assumes 'asc' + + Otherwise there should be just one list arg, elements of + which are each 2-tuples of ``(sortkey, sortdir)`` info:: + + grid.set_sort_defaults([('name', 'asc'), + ('value', 'desc')]) + + .. note:: + + Note that :attr:`sort_multiple` determines whether the grid + is actually allowed to have multiple sort defaults. The + defaults requested by the method call may be pruned if + necessary to accommodate that. Default sorting info is tracked via :attr:`sort_defaults`. """ - self.sort_defaults = [SortInfo(sortkey, sortdir)] + + # convert args to sort defaults + sort_defaults = [] + if len(args) == 1: + if isinstance(args[0], str): + sort_defaults = [SortInfo(args[0], 'asc')] + elif isinstance(args[0], tuple) and len(args[0]) == 2: + sort_defaults = [SortInfo(*args[0])] + elif isinstance(args[0], list): + sort_defaults = [SortInfo(*tup) for tup in args[0]] + else: + raise ValueError("for just one positional arg, must pass string, 2-tuple or list") + elif len(args) == 2: + sort_defaults = [SortInfo(*args)] + else: + raise ValueError("must pass just one or two positional args") + + # prune if multi-column requested but not supported + if len(sort_defaults) > 1 and not self.sort_multiple: + log.warning("multi-column sorting is not enabled for the instance; " + "list will be pruned to first element for '%s' grid: %s", + self.key, sort_defaults) + sort_defaults = [sort_defaults[0]] + + self.sort_defaults = sort_defaults def is_sortable(self, key): """ @@ -795,7 +898,7 @@ class Grid: # configuration methods ############################## - def load_settings(self, store=True): + def load_settings(self, persist=True): """ Load all effective settings for the grid. @@ -821,7 +924,7 @@ class Grid: are saved each time they are loaded. Note that such settings are wiped upon user logout. - :param store: Whether the collected settings should be saved + :param persist: Whether the collected settings should be saved to the user session. """ @@ -829,13 +932,9 @@ class Grid: 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 @@ -858,25 +957,35 @@ class Grid: else: # nothing found in request, so nothing new to save - store = False + persist = False # but still should load whatever is in user session self.update_sort_settings(settings, src='session') self.update_page_settings(settings) - # maybe store settings in user session, for next time - if store: + # maybe save settings in user session, for next time + if persist: self.persist_settings(settings, dest='session') - # update ourself to reflect settings + # update ourself to reflect settings dict.. + + # sorting if self.sortable: - # and self.sort_on_backend: + # nb. doing this for frontend sorting also self.active_sorters = [] for i in range(1, settings['sorters.length'] + 1): self.active_sorters.append({ 'key': settings[f'sorters.{i}.key'], 'dir': settings[f'sorters.{i}.dir'], }) + # TODO: i thought this was needed, but now idk? + # # nb. when showing full index page (i.e. not partial) + # # this implies we must set the default sorter for Vue + # # component, and only single-column is allowed there. + # if not self.request.GET.get('partial'): + # break + + # paging if self.paginated and self.paginate_on_backend: self.pagesize = settings['pagesize'] self.page = settings['page'] @@ -1046,30 +1155,38 @@ class Grid: def sort_data(self, data, sorters=None): """ - Sort the given query according to current settings, and return the result. + Sort the given data and return the result. This is called by + :meth:`get_visible_data()`. - Optional list of sorters to use. If not specified, the grid's - "active" sorter list is used. + :param sorters: Optional list of sorters to use. If not + specified, the grid's :attr:`active_sorters` are used. """ if sorters is None: sorters = self.active_sorters if not sorters: return data - if len(sorters) != 1: - raise NotImplementedError("mulit-column sorting not yet supported") - # our one and only active sorter - sorter = sorters[0] - sortkey = sorter['key'] - sortdir = sorter['dir'] + # 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) - # cannot sort unless we have a sorter callable - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data + for sorter in sorters: + sortkey = sorter['key'] + sortdir = sorter['dir'] - # invoke the sorter - return sortfunc(data, sortdir) + # cannot sort unless we have a sorter callable + sortfunc = self.sorters.get(sortkey) + if not sortfunc: + return data + + # invoke the sorter + data = sortfunc(data, sortdir) + + return data def paginate_data(self, data): """ @@ -1182,6 +1299,29 @@ class Grid: }) return columns + def get_vue_active_sorters(self): + """ + Returns a list of Vue-compatible column sorter definitions. + + The list returned is the same as :attr:`active_sorters`; + however the format used in Vue is different. So this method + just "converts" them to the required format, e.g.:: + + # active_sorters format + {'key': 'name', 'dir': 'asc'} + + # get_vue_active_sorters() format + {'field': 'name', 'order': 'asc'} + + :returns: The :attr:`active_sorters` list, converted as + described above. + """ + sorters = [] + for sorter in self.active_sorters: + sorters.append({'field': sorter['key'], + 'order': sorter['dir']}) + return sorters + def get_vue_data(self): """ Returns a list of Vue-compatible data records. diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako index 98a7ce3..568cfc7 100644 --- a/src/wuttaweb/templates/grids/vue_template.mako +++ b/src/wuttaweb/templates/grids/vue_template.mako @@ -9,12 +9,29 @@ ## sorting % if grid.sortable: - ## nb. buefy only supports *one* default sorter - :default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null" + ## 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 ## paging @@ -119,7 +136,24 @@ ## sorting % if grid.sortable: - sorters: ${json.dumps(grid.active_sorters)|n}, + 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 ## paging @@ -157,6 +191,21 @@ % endif }, + + % 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: { renderNumber(value) { @@ -174,8 +223,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 @@ -226,14 +275,56 @@ field = field.field % endif + % if grid.sort_multiple: + + // 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.sorters = [{field, order}] + + % if grid.sort_multiple: + // multi-column sort not engaged + this.sortingPriority = [] + } + % endif // nb. always reset to first page when sorting changes this.currentPage = 1 this.fetchData() }, + % if grid.sort_multiple: + + sortingPriorityRemoved(field) { + + // 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.fetchData() + }, + + % endif + % endif % if grid.paginated: diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index f766232..cc5af63 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -1124,6 +1124,7 @@ class MasterView(View): kwargs['actions'] = actions kwargs.setdefault('sortable', self.sortable) + kwargs.setdefault('sort_multiple', not self.request.use_oruga) kwargs.setdefault('sort_on_backend', self.sort_on_backend) kwargs.setdefault('sort_defaults', self.sort_defaults) kwargs.setdefault('paginated', self.paginated) diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py index 377f77a..33a0655 100644 --- a/tests/grids/test_base.py +++ b/tests/grids/test_base.py @@ -71,10 +71,19 @@ class TestGrid(WebTestCase): sort_defaults=[('name', 'desc')]) self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) - # sort defaults as list w/ multiple + # multi-column defaults grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_multiple=True, sort_defaults=[('name', 'desc'), ('value', 'asc')]) - self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + self.assertTrue(grid.sort_multiple) + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc'), + mod.SortInfo('value', 'asc')]) + + # multi-column sort disabled for oruga + self.request.use_oruga = True + grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_multiple=True) + self.assertFalse(grid.sort_multiple) def test_vue_tagname(self): grid = self.make_grid() @@ -236,7 +245,7 @@ class TestGrid(WebTestCase): # can skip the saving step self.request.GET = {'pagesize': '10', 'page': '3'} - grid.load_settings(store=False) + grid.load_settings(persist=False) self.assertEqual(grid.page, 3) self.assertEqual(self.request.session['grid.foo.page'], 2) @@ -261,7 +270,7 @@ class TestGrid(WebTestCase): # can skip the saving step self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'} - grid.load_settings(store=False) + grid.load_settings(persist=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') @@ -575,21 +584,21 @@ class TestGrid(WebTestCase): sorted_data = sorter(sample_data, 'asc') self.assertEqual(len(sorted_data), 9) - # case folding is off by default + # case folding is on by default grid = self.make_grid(model_class=model.Setting) sorter = grid.make_sorter('value') sorted_data = sorter(sample_data, 'desc') self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'}) sorted_data = sorter(sample_data, 'asc') - self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'}) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) - # results are different with case folding + # results are different with case folding off grid = self.make_grid(model_class=model.Setting) - sorter = grid.make_sorter('value', foldcase=True) + sorter = grid.make_sorter('value', foldcase=False) sorted_data = sorter(sample_data, 'desc') self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'}) sorted_data = sorter(sample_data, 'asc') - self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo1', 'value': 'ONE'}) def test_set_sorter(self): model = self.app.model @@ -632,13 +641,38 @@ class TestGrid(WebTestCase): def test_set_sort_defaults(self): model = self.app.model - - # basics grid = self.make_grid(model_class=model.Setting, sortable=True) self.assertEqual(grid.sort_defaults, []) - grid.set_sort_defaults('name', 'asc') + + # can set just sortkey + grid.set_sort_defaults('name') self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')]) + # can set sortkey, sortdir + grid.set_sort_defaults('name', 'desc') + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + + # can set sortkey, sortdir as tuple + grid.set_sort_defaults(('value', 'asc')) + self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')]) + + # can set as list + grid.sort_multiple = True + grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')]) + self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc'), + mod.SortInfo('name', 'desc')]) + + # list is pruned if multi-sort disabled + grid.sort_multiple = False + grid.set_sort_defaults([('value', 'asc'), ('name', 'desc')]) + self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')]) + + # error if any other single arg + self.assertRaises(ValueError, grid.set_sort_defaults, 42) + + # error if more than 2 args + self.assertRaises(ValueError, grid.set_sort_defaults, 'name', 'asc', 'value', 'desc') + def test_is_sortable(self): model = self.app.model @@ -701,9 +735,9 @@ class TestGrid(WebTestCase): 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'}, @@ -740,15 +774,26 @@ 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') @@ -823,6 +868,24 @@ class TestGrid(WebTestCase): self.assertEqual(first['field'], 'foo') self.assertEqual(first['label'], 'Foo') + def test_get_vue_active_sorters(self): + model = self.app.model + + # empty + grid = self.make_grid(key='foo', sortable=True, sort_on_backend=True) + grid.load_settings() + sorters = grid.get_vue_active_sorters() + self.assertEqual(sorters, []) + + # format is different + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True, + sort_defaults='name') + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + sorters = grid.get_vue_active_sorters() + self.assertEqual(sorters, [{'field': 'name', 'order': 'asc'}]) + def test_get_vue_data(self): # empty if no columns defined