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:
parent
ec36df4a34
commit
290f8fd51e
|
@ -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,6 +1224,8 @@ class Grid(WuttaGrid):
|
|||
self.request.session.pop(f'{prefix}.sortkey', None)
|
||||
self.request.session.pop(f'{prefix}.sortdir', None)
|
||||
|
||||
# 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')
|
||||
|
@ -1351,12 +1262,14 @@ 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']
|
||||
|
@ -1364,45 +1277,17 @@ class Grid(WuttaGrid):
|
|||
# cannot sort unless we have a sorter callable
|
||||
sortfunc = self.sorters.get(sortkey)
|
||||
if not sortfunc:
|
||||
log.warning("unknown sorter: %s", sorter)
|
||||
continue
|
||||
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)
|
||||
|
||||
# add column/dir to collection
|
||||
query_sorters.append(getattr(sortfunc._column, 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)
|
||||
data = sortfunc(data, sortdir)
|
||||
|
||||
return data
|
||||
|
||||
def paginate_data(self, data):
|
||||
"""
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -83,27 +83,30 @@
|
|||
|
||||
## 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"
|
||||
@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
|
||||
% 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"
|
||||
|
||||
|
||||
## nb. otherwise there may be default multi-column sort
|
||||
:sort-multiple-data="sortingPriority"
|
||||
|
||||
## user must ctrl-click column header to do multi-sort
|
||||
@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):
|
||||
@cellclick="cellClick"
|
||||
|
@ -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
|
||||
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 will only contain multi-column sorters,
|
||||
## but will be *empty* for single-column sorting
|
||||
## nb. this should be empty when current sort is single-column
|
||||
% if len(grid.active_sorters) > 1:
|
||||
sortingPriority: ${json.dumps(grid.active_sorters)|n},
|
||||
sortingPriority: ${json.dumps(grid.get_vue_active_sorters())|n},
|
||||
% else:
|
||||
sortingPriority: [],
|
||||
% endif
|
||||
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
|
||||
## filterable: ${json.dumps(grid.filterable)|n},
|
||||
|
@ -395,15 +399,20 @@
|
|||
},
|
||||
},
|
||||
|
||||
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
|
||||
% 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) {
|
||||
|
@ -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,6 +606,8 @@
|
|||
})
|
||||
},
|
||||
|
||||
% if grid.sortable and grid.sort_on_backend:
|
||||
|
||||
onSort(field, order, event) {
|
||||
|
||||
## nb. buefy passes field name; oruga passes field object
|
||||
|
@ -604,42 +615,58 @@
|
|||
field = field.field
|
||||
% endif
|
||||
|
||||
% if grid.sort_multiple:
|
||||
|
||||
// did user ctrl-click the column header?
|
||||
if (event.ctrlKey) {
|
||||
|
||||
// engage or enhance multi-column sorting
|
||||
const sorter = this.sorters.filter(s => s.key === field)[0]
|
||||
// toggle direction for existing, or add new sorter
|
||||
const sorter = this.sorters.filter(s => s.field === field)[0]
|
||||
if (sorter) {
|
||||
sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc'
|
||||
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
|
||||
} else {
|
||||
this.sorters.push({key: field, dir: order})
|
||||
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
|
||||
|
||||
// always reset to first page when changing sort options
|
||||
// TODO: i mean..right? would we ever not want that?
|
||||
// nb. always reset to first page when sorting changes
|
||||
this.currentPage = 1
|
||||
this.loadAsyncData()
|
||||
},
|
||||
|
||||
% if grid.sort_multiple:
|
||||
|
||||
sortingPriorityRemoved(field) {
|
||||
|
||||
// prune field from active sorters
|
||||
this.sorters = this.sorters.filter(s => s.key !== field)
|
||||
// prune from active sorters
|
||||
this.sorters = this.sorters.filter(s => s.field !== field)
|
||||
|
||||
// nb. must keep active sorter list "as-is" even if
|
||||
// there is only one sorter; buefy seems to expect it
|
||||
// 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
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue