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
|
Boolean indicating whether *any* column sorting is allowed for
|
||||||
the grid. Default is ``False``.
|
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
|
.. attribute:: sort_on_backend
|
||||||
|
|
||||||
|
@ -150,27 +178,64 @@ class Grid:
|
||||||
Only relevant if both :attr:`sortable` and
|
Only relevant if both :attr:`sortable` and
|
||||||
:attr:`sort_on_backend` are true.
|
: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
|
.. attribute:: sort_defaults
|
||||||
|
|
||||||
List of options to be used for default sorting, until the user
|
List of options to be used for default sorting, until the user
|
||||||
requests a different sorting method.
|
requests a different sorting method.
|
||||||
|
|
||||||
This list usually contains either zero or one elements. Each
|
This list usually contains either zero or one elements. (More
|
||||||
element is a :class:`SortInfo` tuple.
|
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.
|
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
|
While the grid logic is built to handle multi-column
|
||||||
sorting, that is not yet fully implemented.
|
sorting, this feature is limited by frontend JS
|
||||||
|
capabilities.
|
||||||
|
|
||||||
Therefore only the *first* element from this list is used
|
Even if ``sort_defaults`` contains multiple entries
|
||||||
for the actual default sorting, regardless.
|
(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
|
.. attribute:: paginated
|
||||||
|
|
||||||
|
@ -232,6 +297,7 @@ class Grid:
|
||||||
actions=[],
|
actions=[],
|
||||||
linked_columns=[],
|
linked_columns=[],
|
||||||
sortable=False,
|
sortable=False,
|
||||||
|
sort_multiple=True,
|
||||||
sort_on_backend=True,
|
sort_on_backend=True,
|
||||||
sorters=None,
|
sorters=None,
|
||||||
sort_defaults=None,
|
sort_defaults=None,
|
||||||
|
@ -258,6 +324,10 @@ class Grid:
|
||||||
|
|
||||||
# sorting
|
# sorting
|
||||||
self.sortable = sortable
|
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
|
self.sort_on_backend = sort_on_backend
|
||||||
if sorters is not None:
|
if sorters is not None:
|
||||||
self.sorters = sorters
|
self.sorters = sorters
|
||||||
|
@ -265,24 +335,7 @@ class Grid:
|
||||||
self.sorters = self.make_backend_sorters()
|
self.sorters = self.make_backend_sorters()
|
||||||
else:
|
else:
|
||||||
self.sorters = {}
|
self.sorters = {}
|
||||||
if sort_defaults:
|
self.set_sort_defaults(sort_defaults or [])
|
||||||
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 = []
|
|
||||||
|
|
||||||
# paging
|
# paging
|
||||||
self.paginated = paginated
|
self.paginated = paginated
|
||||||
|
@ -531,7 +584,7 @@ class Grid:
|
||||||
|
|
||||||
return sorters
|
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
|
Returns a function suitable for use as a backend sorter on the
|
||||||
given column.
|
given column.
|
||||||
|
@ -549,7 +602,9 @@ class Grid:
|
||||||
|
|
||||||
:param foldcase: If the sorter is manual (not SQLAlchemy), and
|
:param foldcase: If the sorter is manual (not SQLAlchemy), and
|
||||||
the column data is of text type, this may be used to
|
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
|
The term "model property" is a bit technical, an example
|
||||||
should help to clarify::
|
should help to clarify::
|
||||||
|
@ -699,20 +754,68 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
self.sorters.pop(key, None)
|
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
|
``args`` for this method are interpreted as follows:
|
||||||
different sorting method.
|
|
||||||
|
|
||||||
: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`.
|
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):
|
def is_sortable(self, key):
|
||||||
"""
|
"""
|
||||||
|
@ -795,7 +898,7 @@ class Grid:
|
||||||
# configuration methods
|
# configuration methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def load_settings(self, store=True):
|
def load_settings(self, persist=True):
|
||||||
"""
|
"""
|
||||||
Load all effective settings for the grid.
|
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 saved each time they are loaded. Note that such settings
|
||||||
are wiped upon user logout.
|
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.
|
to the user session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -829,13 +932,9 @@ class Grid:
|
||||||
settings = {}
|
settings = {}
|
||||||
if self.sortable:
|
if self.sortable:
|
||||||
if self.sort_defaults:
|
if self.sort_defaults:
|
||||||
sort_defaults = self.sort_defaults
|
# nb. as of writing neither Buefy nor Oruga support a
|
||||||
if len(sort_defaults) > 1:
|
# multi-column *default* sort; so just use first sorter
|
||||||
log.warning("multiple sort defaults are not yet supported; "
|
sortinfo = self.sort_defaults[0]
|
||||||
"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'] = sortinfo.sortkey
|
settings['sorters.1.key'] = sortinfo.sortkey
|
||||||
settings['sorters.1.dir'] = sortinfo.sortdir
|
settings['sorters.1.dir'] = sortinfo.sortdir
|
||||||
|
@ -858,25 +957,35 @@ class Grid:
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# nothing found in request, so nothing new to save
|
# nothing found in request, so nothing new to save
|
||||||
store = False
|
persist = False
|
||||||
|
|
||||||
# but still should load whatever is in user session
|
# but still should load whatever is in user session
|
||||||
self.update_sort_settings(settings, src='session')
|
self.update_sort_settings(settings, src='session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
# maybe store settings in user session, for next time
|
# maybe save settings in user session, for next time
|
||||||
if store:
|
if persist:
|
||||||
self.persist_settings(settings, dest='session')
|
self.persist_settings(settings, dest='session')
|
||||||
|
|
||||||
# update ourself to reflect settings
|
# update ourself to reflect settings dict..
|
||||||
|
|
||||||
|
# sorting
|
||||||
if self.sortable:
|
if self.sortable:
|
||||||
# and self.sort_on_backend:
|
# nb. doing this for frontend sorting also
|
||||||
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({
|
||||||
'key': settings[f'sorters.{i}.key'],
|
'key': settings[f'sorters.{i}.key'],
|
||||||
'dir': settings[f'sorters.{i}.dir'],
|
'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:
|
if self.paginated and self.paginate_on_backend:
|
||||||
self.pagesize = settings['pagesize']
|
self.pagesize = settings['pagesize']
|
||||||
self.page = settings['page']
|
self.page = settings['page']
|
||||||
|
@ -1046,20 +1155,26 @@ class Grid:
|
||||||
|
|
||||||
def sort_data(self, data, sorters=None):
|
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
|
:param sorters: Optional list of sorters to use. If not
|
||||||
"active" sorter list is used.
|
specified, the grid's :attr:`active_sorters` are used.
|
||||||
"""
|
"""
|
||||||
if sorters is None:
|
if sorters is None:
|
||||||
sorters = self.active_sorters
|
sorters = self.active_sorters
|
||||||
if not sorters:
|
if not sorters:
|
||||||
return data
|
return data
|
||||||
if len(sorters) != 1:
|
|
||||||
raise NotImplementedError("mulit-column sorting not yet supported")
|
|
||||||
|
|
||||||
# our one and only active sorter
|
# nb. when data is a query, we want to apply sorters in the
|
||||||
sorter = sorters[0]
|
# 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)
|
||||||
|
|
||||||
|
for sorter in sorters:
|
||||||
sortkey = sorter['key']
|
sortkey = sorter['key']
|
||||||
sortdir = sorter['dir']
|
sortdir = sorter['dir']
|
||||||
|
|
||||||
|
@ -1069,7 +1184,9 @@ class Grid:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# invoke the sorter
|
# invoke the sorter
|
||||||
return sortfunc(data, sortdir)
|
data = sortfunc(data, sortdir)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def paginate_data(self, data):
|
def paginate_data(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -1182,6 +1299,29 @@ class Grid:
|
||||||
})
|
})
|
||||||
return columns
|
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):
|
def get_vue_data(self):
|
||||||
"""
|
"""
|
||||||
Returns a list of Vue-compatible data records.
|
Returns a list of Vue-compatible data records.
|
||||||
|
|
|
@ -9,12 +9,29 @@
|
||||||
|
|
||||||
## sorting
|
## sorting
|
||||||
% if grid.sortable:
|
% if grid.sortable:
|
||||||
## nb. buefy only supports *one* default sorter
|
## nb. buefy/oruga only support *one* default sorter
|
||||||
:default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null"
|
:default-sort="sorters.length ? [sorters[0].field, sorters[0].order] : null"
|
||||||
% if grid.sort_on_backend:
|
% if grid.sort_on_backend:
|
||||||
backend-sorting
|
backend-sorting
|
||||||
@sort="onSort"
|
@sort="onSort"
|
||||||
% endif
|
% 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
|
% endif
|
||||||
|
|
||||||
## paging
|
## paging
|
||||||
|
@ -119,7 +136,24 @@
|
||||||
|
|
||||||
## sorting
|
## sorting
|
||||||
% if grid.sortable:
|
% 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
|
% endif
|
||||||
|
|
||||||
## paging
|
## paging
|
||||||
|
@ -157,6 +191,21 @@
|
||||||
|
|
||||||
% endif
|
% 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: {
|
methods: {
|
||||||
|
|
||||||
renderNumber(value) {
|
renderNumber(value) {
|
||||||
|
@ -174,8 +223,8 @@
|
||||||
}
|
}
|
||||||
% if grid.sortable and grid.sort_on_backend:
|
% if grid.sortable and grid.sort_on_backend:
|
||||||
for (let i = 1; i <= this.sorters.length; i++) {
|
for (let i = 1; i <= this.sorters.length; i++) {
|
||||||
params['sort'+i+'key'] = this.sorters[i-1].key
|
params['sort'+i+'key'] = this.sorters[i-1].field
|
||||||
params['sort'+i+'dir'] = this.sorters[i-1].dir
|
params['sort'+i+'dir'] = this.sorters[i-1].order
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
return params
|
return params
|
||||||
|
@ -226,14 +275,56 @@
|
||||||
field = field.field
|
field = field.field
|
||||||
% endif
|
% 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
|
// 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
|
// nb. always reset to first page when sorting changes
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
this.fetchData()
|
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
|
% endif
|
||||||
|
|
||||||
% if grid.paginated:
|
% if grid.paginated:
|
||||||
|
|
|
@ -1124,6 +1124,7 @@ class MasterView(View):
|
||||||
kwargs['actions'] = actions
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
kwargs.setdefault('sortable', self.sortable)
|
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_on_backend', self.sort_on_backend)
|
||||||
kwargs.setdefault('sort_defaults', self.sort_defaults)
|
kwargs.setdefault('sort_defaults', self.sort_defaults)
|
||||||
kwargs.setdefault('paginated', self.paginated)
|
kwargs.setdefault('paginated', self.paginated)
|
||||||
|
|
|
@ -71,10 +71,19 @@ class TestGrid(WebTestCase):
|
||||||
sort_defaults=[('name', 'desc')])
|
sort_defaults=[('name', 'desc')])
|
||||||
self.assertEqual(grid.sort_defaults, [mod.SortInfo('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,
|
grid = self.make_grid(model_class=model.Setting, sortable=True,
|
||||||
|
sort_multiple=True,
|
||||||
sort_defaults=[('name', 'desc'), ('value', 'asc')])
|
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):
|
def test_vue_tagname(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
|
@ -236,7 +245,7 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
# can skip the saving step
|
# can skip the saving step
|
||||||
self.request.GET = {'pagesize': '10', 'page': '3'}
|
self.request.GET = {'pagesize': '10', 'page': '3'}
|
||||||
grid.load_settings(store=False)
|
grid.load_settings(persist=False)
|
||||||
self.assertEqual(grid.page, 3)
|
self.assertEqual(grid.page, 3)
|
||||||
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
self.assertEqual(self.request.session['grid.foo.page'], 2)
|
||||||
|
|
||||||
|
@ -261,7 +270,7 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
# can skip the saving step
|
# can skip the saving step
|
||||||
self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'}
|
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(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.length'], 1)
|
||||||
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
|
self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name')
|
||||||
|
@ -575,21 +584,21 @@ class TestGrid(WebTestCase):
|
||||||
sorted_data = sorter(sample_data, 'asc')
|
sorted_data = sorter(sample_data, 'asc')
|
||||||
self.assertEqual(len(sorted_data), 9)
|
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)
|
grid = self.make_grid(model_class=model.Setting)
|
||||||
sorter = grid.make_sorter('value')
|
sorter = grid.make_sorter('value')
|
||||||
sorted_data = sorter(sample_data, 'desc')
|
sorted_data = sorter(sample_data, 'desc')
|
||||||
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
|
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
|
||||||
sorted_data = sorter(sample_data, 'asc')
|
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)
|
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')
|
sorted_data = sorter(sample_data, 'desc')
|
||||||
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
|
self.assertEqual(dict(sorted_data[0]), {'name': 'foo2', 'value': 'two'})
|
||||||
sorted_data = sorter(sample_data, 'asc')
|
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):
|
def test_set_sorter(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -632,13 +641,38 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
def test_set_sort_defaults(self):
|
def test_set_sort_defaults(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# basics
|
|
||||||
grid = self.make_grid(model_class=model.Setting, sortable=True)
|
grid = self.make_grid(model_class=model.Setting, sortable=True)
|
||||||
self.assertEqual(grid.sort_defaults, [])
|
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')])
|
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):
|
def test_is_sortable(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
@ -701,9 +735,9 @@ class TestGrid(WebTestCase):
|
||||||
sample_data = [
|
sample_data = [
|
||||||
{'name': 'foo1', 'value': 'ONE'},
|
{'name': 'foo1', 'value': 'ONE'},
|
||||||
{'name': 'foo2', 'value': 'two'},
|
{'name': 'foo2', 'value': 'two'},
|
||||||
{'name': 'foo3', 'value': 'three'},
|
{'name': 'foo3', 'value': 'ggg'},
|
||||||
{'name': 'foo4', 'value': 'four'},
|
{'name': 'foo4', 'value': 'ggg'},
|
||||||
{'name': 'foo5', 'value': 'five'},
|
{'name': 'foo5', 'value': 'ggg'},
|
||||||
{'name': 'foo6', 'value': 'six'},
|
{'name': 'foo6', 'value': 'six'},
|
||||||
{'name': 'foo7', 'value': 'seven'},
|
{'name': 'foo7', 'value': 'seven'},
|
||||||
{'name': 'foo8', 'value': 'eight'},
|
{'name': 'foo8', 'value': 'eight'},
|
||||||
|
@ -740,15 +774,26 @@ class TestGrid(WebTestCase):
|
||||||
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
||||||
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
||||||
|
|
||||||
# error if mult-column sort attempted
|
# multi-column sorting for list data
|
||||||
self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[
|
sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'},
|
||||||
{'key': 'name', 'dir': 'desc'},
|
{'key': 'name', 'dir': 'asc'}])
|
||||||
{'key': 'value', '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
|
# cannot sort data if sortfunc is missing for column
|
||||||
grid.remove_sorter('name')
|
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)
|
# nb. sorted data is in same order as original sample (not sorted)
|
||||||
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
self.assertEqual(sorted_data[0]['name'], 'foo1')
|
||||||
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
self.assertEqual(sorted_data[-1]['name'], 'foo9')
|
||||||
|
@ -823,6 +868,24 @@ class TestGrid(WebTestCase):
|
||||||
self.assertEqual(first['field'], 'foo')
|
self.assertEqual(first['field'], 'foo')
|
||||||
self.assertEqual(first['label'], '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):
|
def test_get_vue_data(self):
|
||||||
|
|
||||||
# empty if no columns defined
|
# empty if no columns defined
|
||||||
|
|
Loading…
Reference in a new issue