diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 5e8ba6342ca..b885285ce71 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -9,6 +9,7 @@ from galaxy.datatypes.data import Data from galaxy.util.odict import odict from galaxy.util.bunch import Bunch from galaxy.util.topsort import topsort, topsort_levels, CycleError +from galaxy.workflow.modules import * class WorkflowController( BaseController ): beta = True @@ -31,7 +32,7 @@ class WorkflowController( BaseController ): """ user = trans.get_user() if not workflow_name: - return error( "Must provide a name for the new workflow" ) + error( "Must provide a name for the new workflow" ) # Create the new stored workflow stored_workflow = model.StoredWorkflow() stored_workflow.name = workflow_name @@ -70,13 +71,12 @@ class WorkflowController( BaseController ): rendered by `editor_canvas`. """ if not id: - return trans.show_error_message( "Invalid workflow id" ) + error( "Invalid workflow id" ) id = trans.security.decode_id( id ) - return trans.fill_template( "workflow/editor.mako", - workflow_id=id ) + return trans.fill_template( "workflow/editor.mako", workflow_id=id ) @web.json - def editor_tool_form( self, trans, tool_id=None, **incoming ): + def editor_tool_form( self, trans, type='tool', tool_id=None, **incoming ): """ Accepts a tool state and incoming values, and generates a new tool form and some additional information, packed into a json dictionary. @@ -84,59 +84,39 @@ class WorkflowController( BaseController ): is selected. """ trans.workflow_building_mode = True - tool = trans.app.toolbox.tools_by_id[tool_id] - state = DefaultToolState() - state.decode( incoming.pop("tool_state"), tool, trans.app ) - errors = tool.update_state( trans, tool.inputs, state.inputs, incoming ) - rval = {} - rval['form_html'] = trans.fill_template( "workflow/editor_tool_form.mako", - tool=tool, as_html=as_html, values=state.inputs, errors=errors ) - if errors: - rval['tool_errors'] = errors - else: - rval['tool_errors'] = None - # Updated data_inputs - rval['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) - rval['state'] = state.encode( tool, trans.app ) - return rval + module = module_factory.from_dict( trans, { + 'type': type, + 'tool_id': tool_id, + 'tool_state': incoming.pop("tool_state") + } ) + module.update_state( incoming ) + return { + 'tool_state': module.get_state(), + 'data_inputs': module.get_data_inputs(), + 'data_outputs': module.get_data_outputs(), + 'form_html': module.get_config_form() + } @web.json - def get_tool_info( self, trans, tool_id ): + def get_new_module_info( self, trans, type, **kwargs ): """ - Generate a json dictionary containing info about the tool specified by - `tool_id`. Info includes data inputs and outputs, html representation + Get the info for a new instance of a module initialized with default + paramters (any keyword arguments will be passed along to the module). + Result includes data inputs and outputs, html representation of the initial form, and the initial tool state (with default values). This is called asynchronously whenever a new node is added. """ trans.workflow_building_mode = True - tool = trans.app.toolbox.tools_by_id[tool_id] - rval = {} - rval['name'] = tool.name - rval['tool_id'] = tool.id - data_outputs = [] - for name, ( format, metadata_source, parent ) in tool.outputs.iteritems(): - data_outputs.append( dict( name=name, extension=format ) ) - rval['data_outputs'] = data_outputs - state = tool.new_state( trans, all_pages=True ) - rval['form_html'] = trans.fill_template( "workflow/editor_tool_form.mako", - tool=tool, as_html=as_html, values=state.inputs, errors={} ) - rval['tool_state'] = state.encode( tool, trans.app ) - rval['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) - return rval - - @web.json - def get_module_info( self, trans, type ): - module = module_types[type]() - rval = {} - rval['name'] = module.name - rval['type'] = module.type - rval['tool_state'] = state = module.get_state() - rval['data_inputs'] = module.get_data_inputs() - rval['data_outputs'] = module.get_data_outputs() - rval['form_html'] = trans.fill_template( - "workflow/editor_generic_form.mako", - form = module.get_config_form() ) - return rval + module = module_factory.new( trans, type, **kwargs ) + return { + 'type': module.type, + 'name': module.get_name(), + 'tool_id': module.get_tool_id(), + 'tool_state': module.get_state(), + 'data_inputs': module.get_data_inputs(), + 'data_outputs': module.get_data_outputs(), + 'form_html': module.get_config_form() + } @web.json def load_workflow( self, trans, id ): @@ -158,39 +138,18 @@ class WorkflowController( BaseController ): data['steps'] = {} # For each step, rebuild the form and encode the state for step in workflow.steps: - step_dict = {} - step_dict['id'] = step.order_index - step_dict['type'] = step_type = ( step.type or "tool" ) - if step_type == 'tool': - step_dict['tool_id'] = tool_id = step.tool_id - # Load tool - tool = trans.app.toolbox.tools_by_id[tool_id] - # Build a state from the tool_inputs dict - state = DefaultToolState() - state.inputs = tool.params_from_strings( step.tool_inputs, trans.app, ignore_errors=True ) - step_dict['tool_state'] = state.encode( tool, trans.app ) - # Error messages for the tool - step_dict['tool_errors'] = ( step.tool_errors or None ) - # Input and output specs - step_dict['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) - data_outputs = [] - for name, ( format, metadata_source, parent ) in tool.outputs.iteritems(): - data_outputs.append( dict( name=name, extension=format ) ) - step_dict['data_outputs'] = data_outputs - # Build the tool form html - errors = step.tool_errors - step_dict['form_html'] = trans.fill_template( "workflow/editor_tool_form.mako", - tool=tool, as_html=as_html, values=state.inputs, errors=( step.tool_errors or {} ) ) - step_dict['name'] = tool.name - else: - module = module_types[step.type].from_workflow_step( step ) - step_dict['name'] = module.name - step_dict['tool_state'] = state = module.get_state() - step_dict['data_inputs'] = module.get_data_inputs() - step_dict['data_outputs'] = module.get_data_outputs() - step_dict['form_html'] = trans.fill_template( - "workflow/editor_generic_form.mako", - form = module.get_config_form() ) + module = module_factory.from_workflow_step( trans, step ) + step_dict = { + 'id': step.order_index, + 'type': module.type, + 'tool_id': module.get_tool_id(), + 'name': module.get_name(), + 'tool_state': module.get_state(), + 'tool_errors': module.get_errors(), + 'data_inputs': module.get_data_inputs(), + 'data_outputs': module.get_data_outputs(), + 'form_html': module.get_config_form() + } # Connections input_conn_dict = {} for conn in step.input_connections: @@ -229,25 +188,14 @@ class WorkflowController( BaseController ): for key, step_dict in data['steps'].iteritems(): # Create the model class for the step step = model.WorkflowStep() - step.type = step_type = step_dict['type'] steps.append( step ) steps_by_external_id[ step_dict['id' ] ] = step + # FIXME: Position should be handled inside module step.position = step_dict['position'] - if step_type == 'tool': - step.tool_id = step_dict['tool_id'] - # Decode the tool state from the step dict - tool = trans.app.toolbox.tools_by_id[ step_dict['tool_id'] ] - state = DefaultToolState() - state.decode( step_dict['tool_state'], tool, trans.app ) - # Convert back to strings for database - tool_inputs = tool.params_to_strings( state.inputs, trans.app ) - step.tool_inputs = tool_inputs - step.tool_errors = step_dict.get( 'tool_errors', None ) - if step.tool_errors: - workflow.has_errors = True - else: - module = module_types[step_type].from_state( step_dict['tool_state'] ) - module.save_to_step( step ) + module = module_factory.from_dict( trans, step_dict ) + module.save_to_step( step ) + if step.tool_errors: + workflow.has_errors = True # Stick this in the step temporarily step.temp_input_connections = step_dict['input_connections'] # Second pass to deal with connections between steps @@ -405,15 +353,12 @@ class WorkflowController( BaseController ): workflow = stored.latest_workflow # It is possible for a workflow to have 0 steps if len( workflow.steps ) == 0: - return trans.show_error_message( - "Workflow cannot be run because it does not have any steps" ) + error( "Workflow cannot be run because it does not have any steps" ) #workflow = Workflow.from_simple( simplejson.loads( stored.encoded_value ), trans.app ) if workflow.has_cycles: - return trans.show_error_message( - "Workflow cannot be run because it contains cycles" ) + error( "Workflow cannot be run because it contains cycles" ) if workflow.has_errors: - return trans.show_error_message( - "Workflow cannot be run because of validation errors in some steps" ) + error( "Workflow cannot be run because of validation errors in some steps" ) # Build the state for each step errors = {} if kwargs: @@ -438,7 +383,7 @@ class WorkflowController( BaseController ): step_errors = tool.update_state( trans, tool.inputs, step.state.inputs, step_args, update_only=True, old_errors=old_errors ) else: - module = step.module = WorkflowModule.from_workflow_step( step ) + module = step.module = module_factory.from_workflow_step( trans, step ) state = step.state = module.decode_runtime_state( trans, step_args.pop( "tool_state" ) ) step_errors = module.update_runtime_state( trans, state, step_args ) if step_errors: @@ -495,7 +440,7 @@ class WorkflowController( BaseController ): errors[step.id] = step.tool_errors else: ## Non-tool specific stuff? - step.module = WorkflowModule.from_workflow_step( step ) + step.module = module_factory.from_workflow_step( trans, step ) step.state = step.module.get_runtime_state() # Connections by input name step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) @@ -505,70 +450,6 @@ class WorkflowController( BaseController ): steps=workflow.steps, workflow=stored, errors=errors ) - -## ---- Workflow modules (to be factored out) --------------------------------- - -from elementtree.ElementTree import Element - -## TODO: 'Tool' should be a module rather than a special case - -class WorkflowModule( object ): - @classmethod - def from_workflow_step( cls, step ): - assert step.type in module_types, "Unknown module type" - return module_types[ step.type ].from_workflow_step( step ) - -class InputDataModule( WorkflowModule ): - type = "data_input" - name = "Input dataset" - _inputs = { - 'input' : DataToolParameter( None, Element( "param", name="input", label="Input Dataset", type="data", format="data" ) ) - } - @classmethod - def from_state( cls, state ): - return cls() - @classmethod - def from_workflow_step( cls, step ): - return cls() - def get_state( self ): - return None - def save_to_step( self, step ): - pass - - def get_data_inputs( self ): - return [] - def get_data_outputs( self ): - return [ dict( name='output', extension='input' ) ] - def get_config_form( self ): - return web.FormBuilder( title=self.name ) - - def get_runtime_inputs( self ): - return self._inputs - def get_runtime_state( self ): - state = DefaultToolState() - state.inputs = dict( input=None ) - return state - def encode_runtime_state( self, trans, state ): - fake_tool = Bunch( inputs = self.get_runtime_inputs() ) - return state.encode( fake_tool, trans.app ) - def decode_runtime_state( self, trans, string ): - fake_tool = Bunch( inputs = self.get_runtime_inputs() ) - state = DefaultToolState() - state.decode( string, fake_tool, trans.app ) - return state - def update_runtime_state( self, trans, state, values ): - errors = {} - for name, param in self._inputs.iteritems(): - value, error = check_param( trans, param, values[name], values ) - state.inputs[ name ] = value - if error: - errors[ name ] = error - return errors - - def execute( self, trans, state ): - return dict( output=state.inputs['input']) - -module_types = dict( data_input=InputDataModule ) ## ---- Utility methods ------------------------------------------------------- @@ -589,12 +470,6 @@ def get_stored_workflow( trans, id ): error( "Workflow is not owned by current user" ) # Looks good return stored - -def as_html( param, value, trans, prefix ): - if type( param ) is DataToolParameter: - return "Data input '" + param.name + "' (" + ( " or ".join( param.extensions ) ) + ")" - else: - return param.get_html_field( trans, value ).get_html( prefix ) def attach_ordered_steps( workflow, steps ): ordered_steps = order_workflow_steps( steps ) @@ -700,32 +575,4 @@ def cleanup_param_values( inputs, values ): prefix = "%s|" % ( key ) cleanup( prefix, input.cases[current_case].inputs, group_values ) cleanup( "", inputs, values ) - return associations - -def get_data_inputs( inputs, input_values ): - """ - For a set of input definitions and a tool state, find all of the dataset - inputs - """ - data_inputs = [] - def visitor( inputs, input_values, name_prefix, label_prefix ): - for input in inputs.itervalues(): - if isinstance( input, Repeat ): - for i, d in enumerate( input_values[ input.name ] ): - index = d['__index__'] - new_name_prefix = name_prefix + "%s_%d|" % ( input.name, index ) - new_label_prefix = label_prefix + "%s %d > " % ( input.title, i + 1 ) - visitor( input.inputs, d, new_name_prefix, new_label_prefix ) - elif isinstance( input, Conditional ): - values = input_values[ input.name ] - current = values["__current_case__"] - label_prefix = label_prefix - name_prefix = name_prefix + "|" + input.name - visitor( input.cases[current].inputs, values, name_prefix, label_prefix ) - else: - if isinstance( input, DataToolParameter ): - data_inputs.append( dict( name=name_prefix+input.name, label=label_prefix+input.label, extensions=input.extensions ) ) - visitor( inputs, input_values, "", "" ) - return data_inputs - - + return associations \ No newline at end of file diff --git a/lib/galaxy/workflow/__init__.py b/lib/galaxy/workflow/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py new file mode 100644 index 00000000000..5e9c98e8324 --- /dev/null +++ b/lib/galaxy/workflow/modules.py @@ -0,0 +1,232 @@ +from elementtree.ElementTree import Element + +from galaxy import web +from galaxy.tools.parameters import DataToolParameter, check_param +from galaxy.tools import DefaultToolState +from galaxy.tools.parameters.grouping import Repeat, Conditional +from galaxy.util.bunch import Bunch + +class WorkflowModule( object ): + + def __init__( self, trans ): + self.trans = trans + + ## ---- Creating modules from various representations --------------------- + + @classmethod + def new( Class, trans, tool_id=None ): + """ + Create a new instance of the module with default state + """ + return Class( trans ) + @classmethod + def from_dict( Class, trans, d ): + """ + Create a new instance of the module initialized from values in the + dictionary `d`. + """ + return Class( trans ) + @classmethod + def from_workflow_step( Class, trans, step ): + return Class( trans ) + + ## ---- Saving in various forms ------------------------------------------ + + def save_to_step( self, step ): + step.type = self.type + + ## ---- General attributes ----------------------------------------------- + + def get_type( self ): + return self.type + def get_name( self ): + return self.name + def get_tool_id( self ): + return None + + ## ---- Configuration time ----------------------------------------------- + + def get_state( self ): + return None + def get_errors( self ): + return None + def get_data_inputs( self ): + return [] + def get_data_outputs( self ): + return [] + def update_state( self ): + pass + def get_config_form( self ): + raise TypeError( "Abstract method" ) + + ## ---- Run time --------------------------------------------------------- + + def get_runtime_inputs( self ): + raise TypeError( "Abstract method" ) + def get_runtime_state( self ): + raise TypeError( "Abstract method" ) + def encode_runtime_state( self, trans, state ): + raise TypeError( "Abstract method" ) + def decode_runtime_state( self, trans, string ): + raise TypeError( "Abstract method" ) + def update_runtime_state( self, trans, state, values ): + raise TypeError( "Abstract method" ) + + def execute( self, trans, state ): + raise TypeError( "Abstract method" ) + +class InputDataModule( WorkflowModule ): + type = "data_input" + name = "Input dataset" + _runtime_inputs = { + 'input' : DataToolParameter( None, Element( "param", name="input", label="Input Dataset", type="data", format="data" ) ) + } + + def get_data_inputs( self ): + return [] + def get_data_outputs( self ): + return [ dict( name='output', extension='input' ) ] + def get_config_form( self ): + return self.trans.fill_template( "workflow/editor_generic_form.mako", + form = web.FormBuilder( title=self.name ) ) + + def get_runtime_inputs( self ): + return self._runtime_inputs + def get_runtime_state( self ): + state = DefaultToolState() + state.inputs = dict( input=None ) + return state + def encode_runtime_state( self, trans, state ): + fake_tool = Bunch( inputs = self.get_runtime_inputs() ) + return state.encode( fake_tool, trans.app ) + def decode_runtime_state( self, trans, string ): + fake_tool = Bunch( inputs = self.get_runtime_inputs() ) + state = DefaultToolState() + state.decode( string, fake_tool, trans.app ) + return state + def update_runtime_state( self, trans, state, values ): + errors = {} + for name, param in self._runtime_inputs.iteritems(): + value, error = check_param( trans, param, values.get( name, None ), values ) + state.inputs[ name ] = value + if error: + errors[ name ] = error + return errors + + def execute( self, trans, state ): + return dict( output=state.inputs['input']) + +class ToolModule( object ): + + type = "tool" + + def __init__( self, trans, tool_id ): + self.trans = trans + self.tool_id = tool_id + self.tool = trans.app.toolbox.tools_by_id[ tool_id ] + self.state = None + self.errors = None + + @classmethod + def new( Class, trans, tool_id=None ): + module = Class( trans, tool_id ) + module.state = module.tool.new_state( trans, all_pages=True ) + return module + @classmethod + def from_dict( Class, trans, d ): + tool_id = d['tool_id'] + module = Class( trans, tool_id ) + module.state = DefaultToolState() + module.state.decode( d["tool_state"], module.tool, module.trans.app ) + module.errors = d.get( "tool_errors", None ) + return module + + @classmethod + def from_workflow_step( Class, trans, step ): + tool_id = step.tool_id + module = Class( trans, tool_id ) + module.state = DefaultToolState() + print step.tool_inputs + module.state.inputs = module.tool.params_from_strings( step.tool_inputs, trans.app ) + module.errors = step.tool_errors + return module + + def save_to_step( self, step ): + step.type = self.type + step.tool_id = self.tool_id + step.tool_inputs = self.tool.params_to_strings( self.state.inputs, self.trans.app ) + step.tool_errors = self.errors + + def get_name( self ): + return self.tool.name + def get_tool_id( self ): + return self.tool_id + def get_state( self ): + return self.state.encode( self.tool, self.trans.app ) + def get_errors( self ): + return self.errors + + def get_data_inputs( self ): + data_inputs = [] + def visitor( inputs, input_values, name_prefix, label_prefix ): + for input in inputs.itervalues(): + if isinstance( input, Repeat ): + for i, d in enumerate( input_values[ input.name ] ): + index = d['__index__'] + new_name_prefix = name_prefix + "%s_%d|" % ( input.name, index ) + new_label_prefix = label_prefix + "%s %d > " % ( input.title, i + 1 ) + visitor( input.inputs, d, new_name_prefix, new_label_prefix ) + elif isinstance( input, Conditional ): + values = input_values[ input.name ] + current = values["__current_case__"] + label_prefix = label_prefix + name_prefix = name_prefix + "|" + input.name + visitor( input.cases[current].inputs, values, name_prefix, label_prefix ) + else: + if isinstance( input, DataToolParameter ): + data_inputs.append( dict( name=name_prefix+input.name, label=label_prefix+input.label, extensions=input.extensions ) ) + visitor( self.tool.inputs, self.state.inputs, "", "" ) + return data_inputs + def get_data_outputs( self ): + data_outputs = [] + for name, ( format, metadata_source, parent ) in self.tool.outputs.iteritems(): + data_outputs.append( dict( name=name, extension=format ) ) + return data_outputs + def get_config_form( self ): + def as_html( param, value, trans, prefix ): + if type( param ) is DataToolParameter: + return "Data input '" + param.name + "' (" + ( " or ".join( param.extensions ) ) + ")" + else: + return param.get_html_field( trans, value ).get_html( prefix ) + return self.trans.fill_template( "workflow/editor_tool_form.mako", + tool=self.tool, as_html=as_html, values=self.state.inputs, errors=( self.errors or {} ) ) + def update_state( self, incoming ): + errors = self.tool.update_state( self.trans, self.tool.inputs, self.state.inputs, incoming ) + self.errors = errors or None + + +class WorkflowModuleFactory( object ): + def __init__( self, module_types ): + self.module_types = module_types + def new( self, trans, type, tool_id=None ): + """ + Return module for type and (optional) tool_id intialized with + new / default state. + """ + assert type in self.module_types + return self.module_types[type].new( trans, tool_id ) + def from_dict( self, trans, d ): + """ + Return module initialized from the data in dictionary `d`. + """ + type = d['type'] + assert type in self.module_types + return self.module_types[type].from_dict( trans, d ) + def from_workflow_step( self, trans, step ): + """ + Return module initializd from the WorkflowStep object `step`. + """ + type = step.type + return self.module_types[type].from_workflow_step( trans, step ) + +module_factory = WorkflowModuleFactory( dict( data_input=InputDataModule, tool=ToolModule ) ) \ No newline at end of file diff --git a/static/scripts/galaxy.ui.scrollPanel.js b/static/scripts/galaxy.ui.scrollPanel.js index 5ff7c2a93c4..d4834cb88db 100644 --- a/static/scripts/galaxy.ui.scrollPanel.js +++ b/static/scripts/galaxy.ui.scrollPanel.js @@ -66,15 +66,15 @@ $.ui.plugin.add("draggable", "scrollPanel", { ui.position.top += amount_moved; moved = true; } - // Update offsets - if ( moved ) { - $.ui.ddmanager.prepareOffsets( instance, e ); - } // Still contain in panel ui.position.left = Math.max( ui.position.left, 0 ); ui.position.top = Math.max( ui.position.top, 0 ); ui.position.left = Math.min( ui.position.left, panel_w - element_w ); ui.position.top = Math.min( ui.position.top, panel_h - element_h ); + // Update offsets + if ( moved ) { + $.ui.ddmanager.prepareOffsets( instance, e ); + } // Keep moving even if mouse doesn't move if ( moved ) { instance.old_e = e; diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index 424ee85761b..74cf5467c94 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -295,9 +295,9 @@ $.extend( Node.prototype, { update_field_data : function( data ) { var el = $(this.element), node = this; - this.tool_state = data.state; + this.tool_state = data.tool_state; this.form_html = data.form_html; - this.tool_errors = data.tool_errors; + this.tool_errors = data.tool_errors; if ( this.tool_errors ) { el.addClass( "tool-node-error" ); } else { @@ -493,27 +493,28 @@ function prebuild_node( type, title_text, tool_id ) { scrollSpeed: 20, // containment: $("#shim"), // grow: true, - click: function( _, ui ) { + click: function( e, ui ) { var element = $(this).data("draggable").element.get(0); (function(p) { p.removeChild( element ); p.appendChild( element ) })(element.parentNode) workflow.activate_node( node ); }, - start: function() { + start: function( e, ui ) { workflow.activate_node( node ); $(this).css( 'z-index', '1000' ); }, - drag: function() { + drag: function( e, ui ) { $(this).find( ".terminal" ).each( function() { this.terminal.redraw(); }) }, - stop: function( _, ui ) { + stop: function( e, ui ) { var element = $(this).data("draggable").element.get(0); (function(p) { p.removeChild( element ); p.appendChild( element ) })(element.parentNode) $(this).css( 'z-index', '100' ); $(this).find( ".terminal" ).each( function() { this.terminal.redraw(); }); + workflow.node_changed( this ); } }); return node; diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 81e36ae6a0e..6816c0a0bfa 100644 --- a/templates/workflow/editor.mako +++ b/templates/workflow/editor.mako @@ -28,7 +28,7 @@ $(".dialog-box-container" ).fadeOut( function() { $("#overlay").hide(); } ); }; - function show_modal( title, body, buttons ) { + function show_modal( title, body, buttons, extra_buttons ) { $( ".dialog-box" ).find( ".title" ).html( title ); var b = $( ".dialog-box" ).find( ".buttons" ).html( "" ); if ( buttons ) { @@ -40,6 +40,16 @@ } else { b.hide(); } + var b = $( ".dialog-box" ).find( ".extra_buttons" ).html( "" ); + if ( extra_buttons ) { + $.each( extra_buttons, function( name, value ) { + b.append( $( '