Add basic merge feature to MasterView
				
					
				
			This commit is contained in:
		
							parent
							
								
									a398a0a710
								
							
						
					
					
						commit
						06b0b13992
					
				
					 3 changed files with 251 additions and 4 deletions
				
			
		| 
						 | 
					@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue