feat: add multi-column sorting (frontend or backend) for grids
This commit is contained in:
parent
58f7a862a2
commit
8d6f4ad368
|
@ -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 <https://github.com/oruga-ui/oruga/issues/962>`_
|
||||
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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue