diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 56b4faae..5bb155bf 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -15,7 +15,34 @@ ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))} @@ -26,6 +53,13 @@ % endif -<%def name="grid_tools()"> +<%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')} + + ${h.end_form()} + % endif + ${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n} diff --git a/tailbone/templates/master/merge.mako b/tailbone/templates/master/merge.mako new file mode 100644 index 00000000..6e1abac5 --- /dev/null +++ b/tailbone/templates/master/merge.mako @@ -0,0 +1,135 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base.mako" /> + +<%def name="title()">Merge 2 ${model_title_plural} + +<%def name="head_tags()"> + ${parent.head_tags()} + + + + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to {}".format(model_title_plural), url(route_prefix))}
  • + + + + +

    + You are about to merge 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! +

    + +

    + The ${h.link_to("{} on the right".format(model_title), view_url(object_to_keep), target='_blank', class_='merge-object')} + will be kept + and the ${h.link_to("{} on the left".format(model_title), view_url(object_to_remove), target='_blank', class_='merge-object')} + will be deleted.  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. +

    + + + + + + + + + + + + % for field in sorted(merge_fields): + + + + + + + % endfor + +
    field namedeleting ${model_title}keeping ${model_title}resulting ${model_title}
    ${field}${repr(remove_data[field])}${repr(keep_data[field])}${repr(resulting_data[field])}
    + +${h.form(request.current_route_url(), class_='merge')} +
    + ${h.hidden('uuids', value='{},{}'.format(object_to_remove.uuid, object_to_keep.uuid))} + Whoops, nevermind + + ${h.submit('merge', "Yes, perform this merge")} +
    +${h.end_form()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index a6490129..e9be3536 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -55,6 +55,7 @@ class MasterView(View): viewable = True editable = True deletable = True + mergeable = False listing = False creating = False @@ -394,6 +395,70 @@ class MasterView(View): 'instance_title': instance_title, '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 ############################## @@ -635,7 +700,8 @@ class MasterView(View): 'default_sortkey': getattr(self, 'default_sortkey', None), 'sortdir': getattr(self, 'sortdir', 'asc'), '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, 'row_attrs': self.get_row_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. """ if callable(self.row_attrs): - return self.row_attrs(row, i) - return self.row_attrs + attrs = self.row_attrs(row, i) + else: + attrs = dict(self.row_attrs) + if self.mergeable: + attrs['data-uuid'] = row.uuid + return attrs 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), "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 if cls.viewable: config.add_tailbone_permission(permission_prefix, '{}.view'.format(permission_prefix),