Improve merge support for records with no uuid

for now we "pretend" they have a uuid still, custom view is
responsible for determining the value for each row if needed
This commit is contained in:
Lance Edgar 2023-06-18 18:50:01 -05:00
parent 58354e7adf
commit 214f3d9b1e
4 changed files with 74 additions and 18 deletions

View file

@ -170,6 +170,21 @@ class Grid(object):
'myfield': myrender, 'myfield': myrender,
}, },
) )
.. attribute row_uuid_getter::
Optional callable to obtain the "UUID" (sic) value for each
data row. The default assumption as that each row object has a
``uuid`` attribute, but when that isn't the case, *and* the
grid needs to support checkboxes, we must "pretend" by
injecting some custom value to the ``uuid`` of the row data.
If necssary, set this to a callable like so::
def fake_uuid(row):
return row.some_custom_key
grid.row_uuid_getter = fake_uuid
""" """
def __init__(self, key, data, columns=None, width='auto', request=None, def __init__(self, key, data, columns=None, width='auto', request=None,
@ -182,7 +197,7 @@ class Grid(object):
sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc',
pageable=False, default_pagesize=None, default_page=1, pageable=False, default_pagesize=None, default_page=1,
checkboxes=False, checked=None, check_handler=None, check_all_handler=None, checkboxes=False, checked=None, check_handler=None, check_all_handler=None,
checkable=None, checkable=None, row_uuid_getter=None,
clicking_row_checks_box=False, click_handlers=None, clicking_row_checks_box=False, click_handlers=None,
main_actions=[], more_actions=[], delete_speedbump=False, main_actions=[], more_actions=[], delete_speedbump=False,
ajax_data_url=None, component='tailbone-grid', ajax_data_url=None, component='tailbone-grid',
@ -243,6 +258,7 @@ class Grid(object):
self.check_handler = check_handler self.check_handler = check_handler
self.check_all_handler = check_all_handler self.check_all_handler = check_all_handler
self.checkable = checkable self.checkable = checkable
self.row_uuid_getter = row_uuid_getter
self.clicking_row_checks_box = clicking_row_checks_box self.clicking_row_checks_box = clicking_row_checks_box
self.click_handlers = click_handlers or {} self.click_handlers = click_handlers or {}
@ -1425,6 +1441,16 @@ class Grid(object):
}) })
return columns return columns
def get_uuid_for_row(self, rowobj):
# use custom getter if set
if self.row_uuid_getter:
return self.row_uuid_getter(rowobj)
# otherwise fallback to normal uuid, if present
if hasattr(rowobj, 'uuid'):
return rowobj.uuid
def get_buefy_data(self): def get_buefy_data(self):
""" """
Returns a list of data rows for the grid, for use with Buefy table. Returns a list of data rows for the grid, for use with Buefy table.
@ -1481,8 +1507,9 @@ class Grid(object):
# maybe add UUID for convenience # maybe add UUID for convenience
if 'uuid' not in self.columns: if 'uuid' not in self.columns:
if hasattr(rowobj, 'uuid'): uuid = self.get_uuid_for_row(rowobj)
row['uuid'] = rowobj.uuid if uuid:
row['uuid'] = uuid
# set action URL(s) for row, as needed # set action URL(s) for row, as needed
self.set_action_urls(row, rowobj, i) self.set_action_urls(row, rowobj, i)

View file

@ -192,7 +192,7 @@
% if grid.check_all_handler: % if grid.check_all_handler:
@check-all="${grid.check_all_handler}" @check-all="${grid.check_all_handler}"
% endif % endif
% if isinstance(grid.checkable, six.string_types): % if isinstance(grid.checkable, str):
:is-row-checkable="${grid.row_checkable}" :is-row-checkable="${grid.row_checkable}"
% elif grid.checkable: % elif grid.checkable:
:is-row-checkable="row => row._checkable" :is-row-checkable="row => row._checkable"

View file

@ -123,7 +123,7 @@
<div class="level-item"> <div class="level-item">
${h.form(request.current_route_url(), **{'@submit': 'submitSwapForm'})} ${h.form(request.current_route_url(), **{'@submit': 'submitSwapForm'})}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids', value='{},{}'.format(object_to_keep.uuid, object_to_remove.uuid))} ${h.hidden('uuids', value=f'{keeping_uuid},{removing_uuid}')}
<b-button native-type="submit" <b-button native-type="submit"
:disabled="swapFormSubmitting"> :disabled="swapFormSubmitting">
{{ swapFormButtonText }} {{ swapFormButtonText }}
@ -134,7 +134,7 @@
<div class="level-item"> <div class="level-item">
${h.form(request.current_route_url(), **{'@submit': 'submitMergeForm'})} ${h.form(request.current_route_url(), **{'@submit': 'submitMergeForm'})}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids', value='{},{}'.format(object_to_remove.uuid, object_to_keep.uuid))} ${h.hidden('uuids', value=f'{removing_uuid},{keeping_uuid}')}
${h.hidden('commit-merge', value='yes')} ${h.hidden('commit-merge', value='yes')}
<b-button type="is-primary" <b-button type="is-primary"
native-type="submit" native-type="submit"

View file

@ -441,6 +441,7 @@ class MasterView(View):
'checkable': self.checkbox, 'checkable': self.checkbox,
'clicking_row_checks_box': self.clicking_row_checks_box, 'clicking_row_checks_box': self.clicking_row_checks_box,
'assume_local_times': self.has_local_times, 'assume_local_times': self.has_local_times,
'row_uuid_getter': self.get_uuid_for_grid_row,
} }
if self.sortable or self.pageable or self.filterable: if self.sortable or self.pageable or self.filterable:
@ -453,6 +454,16 @@ class MasterView(View):
defaults.update(kwargs) defaults.update(kwargs)
return defaults return defaults
def get_uuid_for_grid_row(self, obj):
"""
If possible, this should return a "UUID" value to uniquely
identify the given object. Default of course is to use the
actual ``uuid`` attribute of the object, if present. This
value is needed by grids when checkboxes are used.
"""
if hasattr(obj, 'uuid'):
return obj.uuid
def configure_grid(self, grid): def configure_grid(self, grid):
""" """
Perform "final" configuration for the main data grid. Perform "final" configuration for the main data grid.
@ -2046,17 +2057,31 @@ class MasterView(View):
return [] return []
def get_merge_objects(self):
"""
Must return 2 objects, obtained somehow from the request,
which are to be (potentially) merged.
:returns: 2-tuple of ``(object_to_remove, object_to_keep)``,
or ``None``.
"""
uuids = self.request.POST.get('uuids', '').split(',')
if len(uuids) == 2:
cls = self.get_model_class()
object_to_remove = self.Session.get(cls, uuids[0])
object_to_keep = self.Session.get(cls, uuids[1])
if object_to_remove and object_to_keep:
return object_to_remove, object_to_keep
def merge(self): def merge(self):
""" """
Preview and execute a merge of two records. Preview and execute a merge of two records.
""" """
object_to_remove = object_to_keep = None object_to_remove = object_to_keep = None
if self.request.method == 'POST': if self.request.method == 'POST':
uuids = self.request.POST.get('uuids', '').split(',') objects = self.get_merge_objects()
if len(uuids) == 2: if objects:
object_to_remove = self.Session.get(self.get_model_class(), uuids[0]) object_to_remove, object_to_keep = objects
object_to_keep = self.Session.get(self.get_model_class(), uuids[1])
if object_to_remove and object_to_keep and self.request.POST.get('commit-merge') == 'yes': if object_to_remove and object_to_keep and self.request.POST.get('commit-merge') == 'yes':
msg = str(object_to_remove) msg = str(object_to_remove)
try: try:
@ -2073,13 +2098,17 @@ class MasterView(View):
remove = self.get_merge_data(object_to_remove) remove = self.get_merge_data(object_to_remove)
keep = self.get_merge_data(object_to_keep) keep = self.get_merge_data(object_to_keep)
return self.render_to_response('merge', {'object_to_remove': object_to_remove, return self.render_to_response('merge', {
'object_to_remove': object_to_remove,
'object_to_keep': object_to_keep, 'object_to_keep': object_to_keep,
'removing_uuid': self.get_uuid_for_grid_row(object_to_remove),
'keeping_uuid': self.get_uuid_for_grid_row(object_to_keep),
'view_url': lambda obj: self.get_action_url('view', obj), 'view_url': lambda obj: self.get_action_url('view', obj),
'merge_fields': self.get_merge_fields(), 'merge_fields': self.get_merge_fields(),
'remove_data': remove, 'remove_data': remove,
'keep_data': keep, 'keep_data': keep,
'resulting_data': self.get_merge_resulting_data(remove, keep)}) 'resulting_data': self.get_merge_resulting_data(remove, keep),
})
def validate_merge(self, removing, keeping): def validate_merge(self, removing, keeping):
""" """