Add basic merge feature to MasterView
This commit is contained in:
parent
a398a0a710
commit
06b0b13992
|
@ -15,7 +15,34 @@
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))}
|
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
$('.newgrid-wrapper').gridwrapper();
|
$('.newgrid-wrapper').gridwrapper();
|
||||||
|
|
||||||
|
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
|
||||||
|
|
||||||
|
$('form[name="merge-things"] button').button('option', 'disabled', $('.newgrid tbody td.checkbox input:checked').length != 2);
|
||||||
|
|
||||||
|
$('.newgrid-wrapper').on('click', 'tbody td.checkbox input', function() {
|
||||||
|
$('form[name="merge-things"] button').button('option', 'disabled', $('.newgrid tbody td.checkbox input:checked').length != 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$('form[name="merge-things"]').submit(function() {
|
||||||
|
var uuids = [];
|
||||||
|
$('.newgrid tbody td.checkbox input:checked').each(function() {
|
||||||
|
uuids.push($(this).parents('tr:first').data('uuid'));
|
||||||
|
});
|
||||||
|
if (uuids.length != 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$(this).find('[name="uuids"]').val(uuids.toString());
|
||||||
|
$(this).find('button')
|
||||||
|
.button('option', 'label', "Preparing to Merge...")
|
||||||
|
.button('disable');
|
||||||
|
});
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
@ -26,6 +53,13 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="grid_tools()"></%def>
|
<%def name="grid_tools()">
|
||||||
|
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
|
||||||
|
${h.form(url('{}.merge'.format(route_prefix)), name='merge-things')}
|
||||||
|
${h.hidden('uuids')}
|
||||||
|
<button type="submit">Merge 2 ${model_title_plural}</button>
|
||||||
|
${h.end_form()}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
||||||
|
|
135
tailbone/templates/master/merge.mako
Normal file
135
tailbone/templates/master/merge.mako
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Merge 2 ${model_title_plural}</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('button.swap').click(function() {
|
||||||
|
$(this).button('disable').button('option', 'label', "Swapping, please wait...");
|
||||||
|
var form = $(this).parents('form');
|
||||||
|
var input = form.find('input[name="uuids"]');
|
||||||
|
var uuids = input.val().split(',');
|
||||||
|
uuids.reverse();
|
||||||
|
input.val(uuids.join(','));
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form.merge input[type="submit"]').click(function() {
|
||||||
|
$(this).button('disable').button('option', 'label', "Merging, please wait...");
|
||||||
|
var form = $(this).parents('form');
|
||||||
|
form.append($('<input type="hidden" name="commit-merge" value="yes" />'));
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
p {
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
p.warning {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
a.merge-object {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
font-size: 11pt;
|
||||||
|
margin-left: 50px;
|
||||||
|
min-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th,
|
||||||
|
table td {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.value {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.diff tr.diff td.keep-value {
|
||||||
|
background-color: #cfc;
|
||||||
|
}
|
||||||
|
table.diff tr.diff td.remove-value {
|
||||||
|
background-color: #fcc;
|
||||||
|
}
|
||||||
|
table.diff td.result-value.diff {
|
||||||
|
background-color: #fe8;
|
||||||
|
}
|
||||||
|
div.buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
<li>${h.link_to("Back to {}".format(model_title_plural), url(route_prefix))}</li>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<ul id="context-menu">
|
||||||
|
${self.context_menu_items()}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="warning">
|
||||||
|
You are about to <strong>merge</strong> two ${model_title} records,
|
||||||
|
(possibly) along with various related data. The tool you are using now
|
||||||
|
is somewhat generic and is not able to give you the full picture of the
|
||||||
|
implications of this merge. You are urged to proceed with caution!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The ${h.link_to("{} on the right".format(model_title), view_url(object_to_keep), target='_blank', class_='merge-object')}
|
||||||
|
will be <strong>kept</strong>
|
||||||
|
and the ${h.link_to("{} on the left".format(model_title), view_url(object_to_remove), target='_blank', class_='merge-object')}
|
||||||
|
will be <strong>deleted</strong>. The one which is to be kept may also
|
||||||
|
be updated to reflect certain aspects of the one being deleted; however again
|
||||||
|
the details are up to the app logic for this type of merge and aren't fully
|
||||||
|
known to the generic tool which you're using now.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="diff">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>field name</th>
|
||||||
|
<th>deleting ${model_title}</th>
|
||||||
|
<th>keeping ${model_title}</th>
|
||||||
|
<th>resulting ${model_title}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for field in sorted(merge_fields):
|
||||||
|
<tr${' class="diff"' if keep_data[field] != remove_data[field] else ''|n}>
|
||||||
|
<td class="field">${field}</td>
|
||||||
|
<td class="value remove-value">${repr(remove_data[field])}</td>
|
||||||
|
<td class="value keep-value">${repr(keep_data[field])}</td>
|
||||||
|
<td class="value result-value${' diff' if resulting_data[field] != keep_data[field] else ''}">${repr(resulting_data[field])}</td>
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
${h.form(request.current_route_url(), class_='merge')}
|
||||||
|
<div class="buttons">
|
||||||
|
${h.hidden('uuids', value='{},{}'.format(object_to_remove.uuid, object_to_keep.uuid))}
|
||||||
|
<a class="button" href="${index_url}">Whoops, nevermind</a>
|
||||||
|
<button type="button" class="swap">Swap which ${model_title} is kept/removed</button>
|
||||||
|
${h.submit('merge', "Yes, perform this merge")}
|
||||||
|
</div>
|
||||||
|
${h.end_form()}
|
|
@ -55,6 +55,7 @@ class MasterView(View):
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
deletable = True
|
deletable = True
|
||||||
|
mergeable = False
|
||||||
|
|
||||||
listing = False
|
listing = False
|
||||||
creating = False
|
creating = False
|
||||||
|
@ -394,6 +395,70 @@ class MasterView(View):
|
||||||
'instance_title': instance_title,
|
'instance_title': instance_title,
|
||||||
'form': form})
|
'form': form})
|
||||||
|
|
||||||
|
def get_merge_fields(self):
|
||||||
|
if hasattr(self, 'merge_fields'):
|
||||||
|
return self.merge_fields
|
||||||
|
|
||||||
|
def get_merge_coalesce_fields(self):
|
||||||
|
if hasattr(self, 'merge_coalesce_fields'):
|
||||||
|
return self.merge_coalesce_fields
|
||||||
|
return []
|
||||||
|
|
||||||
|
def merge(self):
|
||||||
|
"""
|
||||||
|
Preview and execute a merge of two records.
|
||||||
|
"""
|
||||||
|
object_to_remove = object_to_keep = None
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
uuids = self.request.POST.get('uuids', '').split(',')
|
||||||
|
if len(uuids) == 2:
|
||||||
|
object_to_remove = self.Session.query(self.get_model_class()).get(uuids[0])
|
||||||
|
object_to_keep = self.Session.query(self.get_model_class()).get(uuids[1])
|
||||||
|
|
||||||
|
if object_to_remove and object_to_keep and self.request.POST.get('commit-merge') == 'yes':
|
||||||
|
msg = unicode(object_to_remove)
|
||||||
|
try:
|
||||||
|
self.validate_merge(object_to_remove, object_to_keep)
|
||||||
|
except Exception as error:
|
||||||
|
self.request.session.flash("Requested merge cannot proceed (maybe swap keep/remove and try again?): {}".format(error), 'error')
|
||||||
|
else:
|
||||||
|
self.merge_objects(object_to_remove, object_to_keep)
|
||||||
|
self.request.session.flash("{} has been merged into {}".format(msg, object_to_keep))
|
||||||
|
return self.redirect(self.get_action_url('view', object_to_keep))
|
||||||
|
|
||||||
|
if not object_to_remove or not object_to_keep or object_to_remove is object_to_keep:
|
||||||
|
return self.redirect(self.get_index_url())
|
||||||
|
|
||||||
|
remove = self.get_merge_data(object_to_remove)
|
||||||
|
keep = self.get_merge_data(object_to_keep)
|
||||||
|
return self.render_to_response('merge', {'object_to_remove': object_to_remove,
|
||||||
|
'object_to_keep': object_to_keep,
|
||||||
|
'view_url': lambda obj: self.get_action_url('view', obj),
|
||||||
|
'merge_fields': self.get_merge_fields(),
|
||||||
|
'remove_data': remove,
|
||||||
|
'keep_data': keep,
|
||||||
|
'resulting_data': self.get_merge_resulting_data(remove, keep)})
|
||||||
|
|
||||||
|
def validate_merge(self, removing, keeping):
|
||||||
|
"""
|
||||||
|
If applicable, your view should override this in order to confirm that
|
||||||
|
the requested merge is valid, in your context. If it is not - for *any
|
||||||
|
reason* - you should raise an exception; the type does not matter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_merge_data(self, obj):
|
||||||
|
raise NotImplementedError("please implement `{}.get_merge_data()`".format(self.__class__.__name__))
|
||||||
|
|
||||||
|
def get_merge_resulting_data(self, remove, keep):
|
||||||
|
result = dict(keep)
|
||||||
|
for field in self.get_merge_coalesce_fields():
|
||||||
|
if remove[field] and not keep[field]:
|
||||||
|
result[field] = remove[field]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def merge_objects(self, removing, keeping):
|
||||||
|
raise NotImplementedError("please implement `{}.merge_objects()`".format(self.__class__.__name__))
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# Core Stuff
|
# Core Stuff
|
||||||
##############################
|
##############################
|
||||||
|
@ -635,7 +700,8 @@ class MasterView(View):
|
||||||
'default_sortkey': getattr(self, 'default_sortkey', None),
|
'default_sortkey': getattr(self, 'default_sortkey', None),
|
||||||
'sortdir': getattr(self, 'sortdir', 'asc'),
|
'sortdir': getattr(self, 'sortdir', 'asc'),
|
||||||
'pageable': self.pageable,
|
'pageable': self.pageable,
|
||||||
'checkboxes': self.checkboxes,
|
'checkboxes': self.checkboxes or (
|
||||||
|
self.mergeable and self.request.has_perm('{}.merge'.format(self.get_permission_prefix()))),
|
||||||
'checked': self.checked,
|
'checked': self.checked,
|
||||||
'row_attrs': self.get_row_attrs,
|
'row_attrs': self.get_row_attrs,
|
||||||
'cell_attrs': self.get_cell_attrs,
|
'cell_attrs': self.get_cell_attrs,
|
||||||
|
@ -662,8 +728,12 @@ class MasterView(View):
|
||||||
defined; it depends on the type of data the grid deals with.
|
defined; it depends on the type of data the grid deals with.
|
||||||
"""
|
"""
|
||||||
if callable(self.row_attrs):
|
if callable(self.row_attrs):
|
||||||
return self.row_attrs(row, i)
|
attrs = self.row_attrs(row, i)
|
||||||
return self.row_attrs
|
else:
|
||||||
|
attrs = dict(self.row_attrs)
|
||||||
|
if self.mergeable:
|
||||||
|
attrs['data-uuid'] = row.uuid
|
||||||
|
return attrs
|
||||||
|
|
||||||
def get_cell_attrs(self, row, column):
|
def get_cell_attrs(self, row, column):
|
||||||
"""
|
"""
|
||||||
|
@ -1142,6 +1212,14 @@ class MasterView(View):
|
||||||
config.add_tailbone_permission(permission_prefix, '{0}.create'.format(permission_prefix),
|
config.add_tailbone_permission(permission_prefix, '{0}.create'.format(permission_prefix),
|
||||||
"Create new {0}".format(model_title))
|
"Create new {0}".format(model_title))
|
||||||
|
|
||||||
|
# merge
|
||||||
|
if cls.mergeable:
|
||||||
|
config.add_route('{}.merge'.format(route_prefix), '{}/merge'.format(url_prefix))
|
||||||
|
config.add_view(cls, attr='merge', route_name='{}.merge'.format(route_prefix),
|
||||||
|
permission='{}.merge'.format(permission_prefix))
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.merge'.format(permission_prefix),
|
||||||
|
"Merge 2 {}".format(model_title_plural))
|
||||||
|
|
||||||
# view
|
# view
|
||||||
if cls.viewable:
|
if cls.viewable:
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.view'.format(permission_prefix),
|
config.add_tailbone_permission(permission_prefix, '{}.view'.format(permission_prefix),
|
||||||
|
|
Loading…
Reference in a new issue