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()
|
return self.get_pagesize()
|
||||||
|
|
||||||
def load_settings(self, store=True):
|
def load_settings(self, **kwargs):
|
||||||
"""
|
""" """
|
||||||
Load current/effective settings for the grid, from the request query
|
if 'store' in kwargs:
|
||||||
string and/or session storage. If ``store`` is true, then once
|
warnings.warn("the 'store' param is deprecated for load_settings(); "
|
||||||
settings have been fully read, they are stored in current session for
|
"please use the 'persist' param instead",
|
||||||
next time. Finally, various instance attributes of the grid and its
|
DeprecationWarning, stacklevel=2)
|
||||||
filters are updated in-place to reflect the settings; this is so code
|
kwargs.setdefault('persist', kwargs.pop('store'))
|
||||||
needn't access the settings dict directly, but the more Pythonic
|
|
||||||
instance attributes.
|
persist = kwargs.get('persist', True)
|
||||||
"""
|
|
||||||
|
|
||||||
# initial default settings
|
# initial default settings
|
||||||
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
|
||||||
|
@ -900,16 +895,16 @@ class Grid(WuttaGrid):
|
||||||
elif self.filterable and self.request_has_settings('filter'):
|
elif self.filterable and self.request_has_settings('filter'):
|
||||||
self.update_filter_settings(settings, 'request')
|
self.update_filter_settings(settings, 'request')
|
||||||
if self.request_has_settings('sort'):
|
if self.request_has_settings('sort'):
|
||||||
self.update_sort_settings(settings, 'request')
|
self.update_sort_settings(settings, src='request')
|
||||||
else:
|
else:
|
||||||
self.update_sort_settings(settings, 'session')
|
self.update_sort_settings(settings, src='session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
# If request has no filter settings but does have sort settings, grab
|
# If request has no filter settings but does have sort settings, grab
|
||||||
# those, then grab filter settings from session, then grab pager
|
# those, then grab filter settings from session, then grab pager
|
||||||
# settings from request or session.
|
# settings from request or session.
|
||||||
elif self.request_has_settings('sort'):
|
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_filter_settings(settings, 'session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
|
@ -921,26 +916,26 @@ class Grid(WuttaGrid):
|
||||||
elif self.request_has_settings('page'):
|
elif self.request_has_settings('page'):
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
self.update_filter_settings(settings, 'session')
|
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.
|
# If request has no settings, grab all from session.
|
||||||
elif self.session_has_settings():
|
elif self.session_has_settings():
|
||||||
self.update_filter_settings(settings, 'session')
|
self.update_filter_settings(settings, 'session')
|
||||||
self.update_sort_settings(settings, 'session')
|
self.update_sort_settings(settings, src='session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
# If no settings were found in request or session, don't store result.
|
# If no settings were found in request or session, don't store result.
|
||||||
else:
|
else:
|
||||||
store = False
|
persist = False
|
||||||
|
|
||||||
# Maybe store settings for next time.
|
# Maybe store settings for next time.
|
||||||
if store:
|
if persist:
|
||||||
self.persist_settings(settings, 'session')
|
self.persist_settings(settings, dest='session')
|
||||||
|
|
||||||
# If request contained instruction to save current settings as defaults
|
# If request contained instruction to save current settings as defaults
|
||||||
# for the current user, then do that.
|
# for the current user, then do that.
|
||||||
if self.request.GET.get('save-current-filters-as-defaults') == 'true':
|
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
|
# update ourself to reflect settings
|
||||||
if self.filterable:
|
if self.filterable:
|
||||||
|
@ -1107,44 +1102,6 @@ class Grid(WuttaGrid):
|
||||||
return any([key.startswith(f'{prefix}.filter')
|
return any([key.startswith(f'{prefix}.filter')
|
||||||
for key in self.request.session])
|
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):
|
def update_filter_settings(self, settings, source):
|
||||||
"""
|
"""
|
||||||
Updates a settings dictionary according to filter settings data found
|
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
|
# consider filter active if query string contains a value for it
|
||||||
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
|
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
|
||||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
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(
|
settings['{}.value'.format(prefix)] = self.get_setting(
|
||||||
source, settings, filtr.key, default='')
|
settings, filtr.key, src='request', default='')
|
||||||
|
|
||||||
else: # source = session
|
else: # source = session
|
||||||
settings['{}.active'.format(prefix)] = self.get_setting(
|
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)
|
normalize=lambda v: str(v).lower() == 'true', default=False)
|
||||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
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(
|
settings['{}.value'.format(prefix)] = self.get_setting(
|
||||||
source, settings, '{}.value'.format(prefix), default='')
|
settings, f'{prefix}.value', src='session', 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)
|
|
||||||
|
|
||||||
def update_page_settings(self, settings):
|
def update_page_settings(self, settings):
|
||||||
"""
|
"""
|
||||||
|
@ -1264,18 +1168,19 @@ class Grid(WuttaGrid):
|
||||||
if page is not None:
|
if page is not None:
|
||||||
settings['page'] = int(page)
|
settings['page'] = int(page)
|
||||||
|
|
||||||
def persist_settings(self, settings, to='session'):
|
def persist_settings(self, settings, dest='session'):
|
||||||
"""
|
""" """
|
||||||
Persist the given settings in some way, as defined by ``func``.
|
if dest not in ('defaults', 'session'):
|
||||||
"""
|
raise ValueError(f"invalid dest identifier: {dest}")
|
||||||
|
|
||||||
app = self.request.rattail_config.get_app()
|
app = self.request.rattail_config.get_app()
|
||||||
model = app.model
|
model = app.model
|
||||||
|
|
||||||
def persist(key, value=lambda k: settings[k]):
|
def persist(key, value=lambda k: settings.get(k)):
|
||||||
if to == 'defaults':
|
if dest == 'defaults':
|
||||||
skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
|
skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
|
||||||
app.save_setting(Session(), skey, value(key))
|
app.save_setting(Session(), skey, value(key))
|
||||||
else: # to == session
|
else: # dest == session
|
||||||
skey = 'grid.{}.{}'.format(self.key, key)
|
skey = 'grid.{}.{}'.format(self.key, key)
|
||||||
self.request.session[skey] = value(key)
|
self.request.session[skey] = value(key)
|
||||||
|
|
||||||
|
@ -1287,9 +1192,11 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
if self.sortable:
|
if self.sortable:
|
||||||
|
|
||||||
# first clear existing settings for *sorting* only
|
# first must clear all sort settings from dest. this is
|
||||||
# nb. this is because number of sort settings will vary
|
# because number of sort settings will vary, so we delete
|
||||||
if to == 'defaults':
|
# all and then write all
|
||||||
|
|
||||||
|
if dest == 'defaults':
|
||||||
prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
|
prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}'
|
||||||
query = Session.query(model.Setting)\
|
query = Session.query(model.Setting)\
|
||||||
.filter(sa.or_(
|
.filter(sa.or_(
|
||||||
|
@ -1303,7 +1210,9 @@ class Grid(WuttaGrid):
|
||||||
for setting in query.all():
|
for setting in query.all():
|
||||||
Session.delete(setting)
|
Session.delete(setting)
|
||||||
Session.flush()
|
Session.flush()
|
||||||
|
|
||||||
else: # session
|
else: # session
|
||||||
|
# remove sort settings from user session
|
||||||
prefix = f'grid.{self.key}'
|
prefix = f'grid.{self.key}'
|
||||||
for key in list(self.request.session):
|
for key in list(self.request.session):
|
||||||
if key.startswith(f'{prefix}.sorters.'):
|
if key.startswith(f'{prefix}.sorters.'):
|
||||||
|
@ -1315,10 +1224,12 @@ class Grid(WuttaGrid):
|
||||||
self.request.session.pop(f'{prefix}.sortkey', None)
|
self.request.session.pop(f'{prefix}.sortkey', None)
|
||||||
self.request.session.pop(f'{prefix}.sortdir', None)
|
self.request.session.pop(f'{prefix}.sortdir', None)
|
||||||
|
|
||||||
persist('sorters.length')
|
# now save sort settings to dest
|
||||||
for i in range(1, settings['sorters.length'] + 1):
|
if 'sorters.length' in settings:
|
||||||
persist(f'sorters.{i}.key')
|
persist('sorters.length')
|
||||||
persist(f'sorters.{i}.dir')
|
for i in range(1, settings['sorters.length'] + 1):
|
||||||
|
persist(f'sorters.{i}.key')
|
||||||
|
persist(f'sorters.{i}.dir')
|
||||||
|
|
||||||
if self.paginated:
|
if self.paginated:
|
||||||
persist('pagesize')
|
persist('pagesize')
|
||||||
|
@ -1351,58 +1262,32 @@ class Grid(WuttaGrid):
|
||||||
if not sorters:
|
if not sorters:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# sqlalchemy queries require special handling, in case of
|
# nb. when data is a query, we want to apply sorters in the
|
||||||
# multi-column sorting
|
# requested order, so the final query has order_by() in the
|
||||||
if isinstance(data, orm.Query):
|
# 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
|
for sorter in sorters:
|
||||||
query_sorters = []
|
sortkey = sorter['key']
|
||||||
for sorter in sorters:
|
sortdir = sorter['dir']
|
||||||
sortkey = sorter['key']
|
|
||||||
sortdir = sorter['dir']
|
|
||||||
|
|
||||||
# cannot sort unless we have a sorter callable
|
# cannot sort unless we have a sorter callable
|
||||||
sortfunc = self.sorters.get(sortkey)
|
sortfunc = self.sorters.get(sortkey)
|
||||||
if not sortfunc:
|
if not sortfunc:
|
||||||
log.warning("unknown sorter: %s", sorter)
|
return data
|
||||||
continue
|
|
||||||
|
|
||||||
# join appropriate model if needed
|
# join appropriate model if needed
|
||||||
if sortkey in self.joiners and sortkey not in self.joined:
|
if sortkey in self.joiners and sortkey not in self.joined:
|
||||||
data = self.joiners[sortkey](data)
|
data = self.joiners[sortkey](data)
|
||||||
self.joined.add(sortkey)
|
self.joined.add(sortkey)
|
||||||
|
|
||||||
# add column/dir to collection
|
# invoke the sorter
|
||||||
query_sorters.append(getattr(sortfunc._column, sortdir)())
|
data = sortfunc(data, sortdir)
|
||||||
|
|
||||||
# apply sorting to query
|
return data
|
||||||
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)
|
|
||||||
|
|
||||||
def paginate_data(self, data):
|
def paginate_data(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -658,19 +658,19 @@
|
||||||
## TODO: is there a better way to check if viewing parent?
|
## TODO: is there a better way to check if viewing parent?
|
||||||
% if parent_instance is Undefined:
|
% if parent_instance is Undefined:
|
||||||
% if master.editable and instance_editable and master.has_perm('edit'):
|
% 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"
|
icon-left="edit"
|
||||||
text="Edit This">
|
text="Edit This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.cloneable and master.has_perm('clone'):
|
% if getattr(master, 'cloneable', False) and master.has_perm('clone'):
|
||||||
<once-button tag="a" href="${action_url('clone', instance)}"
|
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
|
||||||
icon-left="object-ungroup"
|
icon-left="object-ungroup"
|
||||||
text="Clone This">
|
text="Clone This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.deletable and instance_deletable and master.has_perm('delete'):
|
% 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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -679,7 +679,7 @@
|
||||||
% else:
|
% else:
|
||||||
## viewing row
|
## viewing row
|
||||||
% if instance_deletable and master.has_perm('delete_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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -688,13 +688,13 @@
|
||||||
% endif
|
% endif
|
||||||
% elif master and master.editing:
|
% elif master and master.editing:
|
||||||
% if master.viewable and master.has_perm('view'):
|
% 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"
|
icon-left="eye"
|
||||||
text="View This">
|
text="View This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.deletable and instance_deletable and master.has_perm('delete'):
|
% 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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -702,13 +702,13 @@
|
||||||
% endif
|
% endif
|
||||||
% elif master and master.deleting:
|
% elif master and master.deleting:
|
||||||
% if master.viewable and master.has_perm('view'):
|
% 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"
|
icon-left="eye"
|
||||||
text="View This">
|
text="View This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.editable and instance_editable and master.has_perm('edit'):
|
% 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"
|
icon-left="edit"
|
||||||
text="Edit This">
|
text="Edit This">
|
||||||
</once-button>
|
</once-button>
|
||||||
|
|
|
@ -83,26 +83,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:
|
||||||
backend-sorting
|
backend-sorting
|
||||||
@sort="onSort"
|
@sort="onSort"
|
||||||
@sorting-priority-removed="sortingPriorityRemoved"
|
% endif
|
||||||
|
% if grid.sort_multiple:
|
||||||
## TODO: there is a bug (?) which prevents the arrow from
|
% if grid.sort_on_backend:
|
||||||
## displaying for simple default single-column sort. so to
|
## TODO: there is a bug (?) which prevents the arrow
|
||||||
## work around that, we *disable* multi-sort until the
|
## from displaying for simple default single-column sort,
|
||||||
## component is mounted. seems to work for now..see also
|
## when multi-column sort is allowed for the table. for
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## now we work around that by waiting until mount to
|
||||||
:sort-multiple="allowMultiSort"
|
## 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"
|
||||||
:sort-multiple-data="sortingPriority"
|
@sorting-priority-removed="sortingPriorityRemoved"
|
||||||
|
% else:
|
||||||
## user must ctrl-click column header to do multi-sort
|
sort-multiple
|
||||||
sort-multiple-key="ctrlKey"
|
% endif
|
||||||
|
## nb. user must ctrl-click column header for multi-sort
|
||||||
|
sort-multiple-key="ctrlKey"
|
||||||
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if getattr(grid, 'click_handlers', None):
|
% if getattr(grid, 'click_handlers', None):
|
||||||
|
@ -276,23 +279,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:
|
||||||
## TODO: there is a bug (?) which prevents the arrow from
|
% if grid.sort_on_backend:
|
||||||
## displaying for simple default single-column sort. so to
|
## TODO: there is a bug (?) which prevents the arrow
|
||||||
## work around that, we *disable* multi-sort until the
|
## from displaying for simple default single-column sort,
|
||||||
## component is mounted. seems to work for now..see also
|
## when multi-column sort is allowed for the table. for
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## now we work around that by waiting until mount to
|
||||||
allowMultiSort: false,
|
## enable the multi-column support. see also
|
||||||
|
## https://github.com/buefy/buefy/issues/2584
|
||||||
## nb. this will only contain multi-column sorters,
|
allowMultiSort: false,
|
||||||
## 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:
|
% if len(grid.active_sorters) > 1:
|
||||||
sortingPriority: ${json.dumps(grid.active_sorters)|n},
|
sortingPriority: ${json.dumps(grid.get_vue_active_sorters())|n},
|
||||||
% else:
|
% else:
|
||||||
sortingPriority: [],
|
sortingPriority: [],
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## filterable: ${json.dumps(grid.filterable)|n},
|
## filterable: ${json.dumps(grid.filterable)|n},
|
||||||
|
@ -395,14 +399,19 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
% 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. so to
|
## TODO: there is a bug (?) which prevents the arrow
|
||||||
## work around that, we *disable* multi-sort until the
|
## from displaying for simple default single-column sort,
|
||||||
## component is mounted. seems to work for now..see also
|
## when multi-column sort is allowed for the table. for
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## now we work around that by waiting until mount to
|
||||||
this.allowMultiSort = true
|
## enable the multi-column support. see also
|
||||||
},
|
## https://github.com/buefy/buefy/issues/2584
|
||||||
|
mounted() {
|
||||||
|
this.allowMultiSort = true
|
||||||
|
},
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
@ -483,8 +492,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
|
||||||
|
@ -597,48 +606,66 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onSort(field, order, event) {
|
% if grid.sortable and grid.sort_on_backend:
|
||||||
|
|
||||||
## nb. buefy passes field name; oruga passes field object
|
onSort(field, order, event) {
|
||||||
% if request.use_oruga:
|
|
||||||
field = field.field
|
|
||||||
% endif
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
## nb. buefy passes field name; oruga passes field object
|
||||||
|
% if request.use_oruga:
|
||||||
|
field = field.field
|
||||||
|
% endif
|
||||||
|
|
||||||
// engage or enhance multi-column sorting
|
% if grid.sort_multiple:
|
||||||
const sorter = this.sorters.filter(s => s.key === field)[0]
|
|
||||||
if (sorter) {
|
|
||||||
sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc'
|
|
||||||
} else {
|
|
||||||
this.sorters.push({key: field, dir: order})
|
|
||||||
}
|
|
||||||
this.sortingPriority = this.sorters
|
|
||||||
|
|
||||||
} else {
|
// did user ctrl-click the column header?
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
|
||||||
|
// toggle direction for existing, or add new sorter
|
||||||
|
const sorter = this.sorters.filter(s => s.field === field)[0]
|
||||||
|
if (sorter) {
|
||||||
|
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
|
||||||
|
} else {
|
||||||
|
this.sorters.push({field, order})
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply multi-column sorting
|
||||||
|
this.sortingPriority = this.sorters
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
// sort by single column only
|
// sort by single column only
|
||||||
this.sorters = [{key: field, dir: order}]
|
this.sorters = [{field, order}]
|
||||||
this.sortingPriority = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// always reset to first page when changing sort options
|
% if grid.sort_multiple:
|
||||||
// TODO: i mean..right? would we ever not want that?
|
// multi-column sort not engaged
|
||||||
this.currentPage = 1
|
this.sortingPriority = []
|
||||||
this.loadAsyncData()
|
}
|
||||||
},
|
% endif
|
||||||
|
|
||||||
sortingPriorityRemoved(field) {
|
// nb. always reset to first page when sorting changes
|
||||||
|
this.currentPage = 1
|
||||||
|
this.loadAsyncData()
|
||||||
|
},
|
||||||
|
|
||||||
// prune field from active sorters
|
% if grid.sort_multiple:
|
||||||
this.sorters = this.sorters.filter(s => s.key !== field)
|
|
||||||
|
|
||||||
// nb. must keep active sorter list "as-is" even if
|
sortingPriorityRemoved(field) {
|
||||||
// there is only one sorter; buefy seems to expect it
|
|
||||||
this.sortingPriority = this.sorters
|
|
||||||
|
|
||||||
this.loadAsyncData()
|
// prune from active sorters
|
||||||
},
|
this.sorters = this.sorters.filter(s => s.field !== field)
|
||||||
|
|
||||||
|
// nb. even though we might have just one sorter
|
||||||
|
// now, we are still technically in multi-sort mode
|
||||||
|
this.sortingPriority = this.sorters
|
||||||
|
|
||||||
|
this.loadAsyncData()
|
||||||
|
},
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
resetView() {
|
resetView() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
|
@ -341,7 +341,7 @@ class MasterView(View):
|
||||||
return self.redirect(self.request.current_route_url(**kw))
|
return self.redirect(self.request.current_route_url(**kw))
|
||||||
|
|
||||||
# Stash some grid stats, for possible use when generating URLs.
|
# 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
|
self.first_visible_grid_index = grid.pager.first_item
|
||||||
|
|
||||||
# return grid data only, if partial page was requested
|
# return grid data only, if partial page was requested
|
||||||
|
@ -442,6 +442,7 @@ class MasterView(View):
|
||||||
'filterable': self.filterable,
|
'filterable': self.filterable,
|
||||||
'use_byte_string_filters': self.use_byte_string_filters,
|
'use_byte_string_filters': self.use_byte_string_filters,
|
||||||
'sortable': self.sortable,
|
'sortable': self.sortable,
|
||||||
|
'sort_multiple': not self.request.use_oruga,
|
||||||
'paginated': self.pageable,
|
'paginated': self.pageable,
|
||||||
'extra_row_class': self.grid_extra_class,
|
'extra_row_class': self.grid_extra_class,
|
||||||
'url': lambda obj: self.get_action_url('view', obj),
|
'url': lambda obj: self.get_action_url('view', obj),
|
||||||
|
|
|
@ -388,14 +388,63 @@ class TestGrid(WebTestCase):
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}])
|
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):
|
def test_sort_data(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
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'},
|
||||||
|
@ -432,32 +481,30 @@ 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')
|
||||||
|
|
||||||
# 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):
|
def test_render_vue_tag(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue