Cleaning up ColumnListParameter and related things.

- Dependencies between parameters and refresh_on_change are now more general.
   Any parameter can assert a dependency on any other parameter by name, and
   if a parameter has dependencies it will cause a refresh.

 - Use "other_values" to get at the value of the associated dataset rather
   than trying to look at the tool. This means that column lists now work
   inside grouping constructs.

 - Eliminated tool.updated_params and use of self.legal_values. These
   variables are GLOBAL. Everybody using a tool has a reference to the same
   instance of tool. Thus, if two people are using the same tool at once this
   can cause serious problems.
This commit is contained in:
James Taylor
2007-10-08 19:33:23 +00:00
parent fd19134676
commit 13608b5a8b
4 changed files with 118 additions and 89 deletions
+13 -9
View File
@@ -172,7 +172,6 @@ class Tool:
self.config_file = config_file
self.tool_dir = os.path.dirname( config_file )
self.app = app
self.updated_params = {}
# Parse XML element containing configuration
self.parse( root )
@@ -408,20 +407,21 @@ class Tool:
display = None
return display, inputs
def parse_input_elem( self, parent_elem, enctypes ):
def parse_input_elem( self, parent_elem, enctypes, context=None ):
"""
Parse a parent element whose children are inputs -- these could be
groups (repeat, conditional) or param elements. Groups will be parsed
recursively.
"""
rval = odict()
context = ExpressionContext( rval, context )
for elem in parent_elem:
# Repeat group
if elem.tag == "repeat":
group = Repeat()
group.name = elem.get( "name" )
group.title = elem.get( "title" )
group.inputs = self.parse_input_elem( elem, enctypes )
group.inputs = self.parse_input_elem( elem, enctypes, context )
rval[group.name] = group
elif elem.tag == "conditional":
group = Conditional()
@@ -429,20 +429,22 @@ class Tool:
# Should have one child "input" which determines the case
input_elem = elem.find( "param" )
assert input_elem is not None, "<conditional> must have a child <param>"
group.test_param = self.parse_param_elem( input_elem, enctypes )
group.test_param = self.parse_param_elem( input_elem, enctypes, context )
# Must refresh when test_param changes
group.test_param.refresh_on_change = True
# And a set of possible cases
for case_elem in elem.findall( "when" ):
case = ConditionalWhen()
case.value = case_elem.get( "value" )
case.inputs = self.parse_input_elem( case_elem, enctypes )
case.inputs = self.parse_input_elem( case_elem, enctypes, context )
group.cases.append( case )
rval[group.name] = group
elif elem.tag == "param":
param = self.parse_param_elem( elem, enctypes )
param = self.parse_param_elem( elem, enctypes, context )
rval[param.name] = param
return rval
def parse_param_elem( self, input_elem, enctypes ):
def parse_param_elem( self, input_elem, enctypes, context ):
"""
Parse a single "<param>" element and return a ToolParameter instance.
Also, if the parameter has a 'required_enctype' add it to the set
@@ -452,6 +454,10 @@ class Tool:
param_enctype = param.get_required_enctype()
if param_enctype:
enctypes.add( param_enctype )
# If parameter depends on any other paramters, we must refresh the
# form when it changes
for name in param.get_dependencies():
context[ name ].refresh_on_change = True
return param
def new_state( self, trans ):
@@ -461,7 +467,6 @@ class Tool:
"""
state = DefaultToolState()
state.inputs = {}
self.updated_params = {}
self.fill_in_new_state( trans, self.inputs_by_page[ 0 ], state.inputs )
return state
@@ -662,7 +667,6 @@ class Tool:
if error:
errors[ input.name ] = error
state[input.name] = value
self.updated_params[input.name] = value
return errors
def check_param( self, trans, param, incoming_value, param_values ):
+99 -69
View File
@@ -21,8 +21,9 @@ class ToolParameter( object ):
moment but in the future should encapsulate more complex parameters (lists
of valid choices, validation logic, ...)
"""
def __init__( self, tool, param ):
def __init__( self, tool, param, context=None ):
self.tool = tool
self.refresh_on_change = False
self.name = param.get("name")
self.type = param.get("type")
self.label = util.xml_text(param, "label")
@@ -70,6 +71,12 @@ class ToolParameter( object ):
"""
return None
def get_dependencies( self ):
"""
Return the names of any other parameters this parameter depends on
"""
return []
def filter_value( self, value, trans=None, other_values={} ):
"""
Parse the value returned by the view into a form usable by the tool OR
@@ -99,6 +106,13 @@ class ToolParameter( object ):
return value
else:
return self.to_python( value, app )
def value_to_display_text( self, value, app ):
"""
Convert a value to a text representation suitable for displaying to
the user
"""
return value
def to_param_dict_string( self, value ):
return str( value )
@@ -108,7 +122,7 @@ class ToolParameter( object ):
validator.validate( value, history )
@classmethod
def build( cls, tool, param):
def build( cls, tool, param ):
"""Factory method to create parameter of correct type"""
param_type = param.get("type")
if not param_type or param_type not in parameter_types:
@@ -415,7 +429,7 @@ class SelectToolParameter( ToolParameter ):
return eval( self.dynamic_options, self.tool.code_namespace, other_values )
else:
return self.options
def get_legal_values( self, other_values ):
def get_legal_values( self, trans, other_values ):
if self.dynamic_options:
return set( v for _, v, _ in eval( self.dynamic_options, self.tool.code_namespace, other_values ) )
else:
@@ -431,19 +445,21 @@ class SelectToolParameter( ToolParameter ):
field.add_option( text, optval, selected )
return field
def from_html( self, value, trans=None, other_values={} ):
legal_values = self.get_legal_values( other_values )
legal_values = self.get_legal_values( trans, other_values )
if isinstance( value, list ):
if not(self.repeat):
assert self.multiple, "Multiple values provided but parameter is not expecting multiple values"
rval = []
for v in value:
v = util.restore_text( v )
assert v in legal_values
if v not in legal_values:
raise ValueError( "An invalid option was selected, please verify" )
rval.append( v )
return rval
else:
value = util.restore_text( value )
assert value in legal_values
if value not in legal_values:
raise ValueError( "An invalid option was selected, please verify" )
return value
def to_param_dict_string( self, value ):
if value is None:
@@ -509,7 +525,7 @@ class GenomeBuildParameter( SelectToolParameter ):
last_used_build = trans.history.genome_build
for dbkey, build_name in util.dbnames:
yield build_name, dbkey, ( dbkey == last_used_build )
def get_legal_values( self, other_values ):
def get_legal_values( self, trans, other_values ):
return set( dbkey for dbkey, _ in util.dbnames )
class ColumnListParameter( SelectToolParameter ):
@@ -535,71 +551,47 @@ class ColumnListParameter( SelectToolParameter ):
"""
def __init__( self, tool, elem ):
SelectToolParameter.__init__( self, tool, elem )
self.tool = tool
self.numerical = str_bool( elem.get( "numerical", False ))
self.assoc_dataset = elem.get( "assoc_dataset", None )
def get_options( self, trans, other_values ):
columnList = []
dataset_id = 0
dataset = None
assoc_dataset = self.tool.get_param( self.assoc_dataset )
assert assoc_dataset is not None, "Tools that include a ColumnListParameter must also include a DataToolParameter"
if len( self.tool.updated_params ) == 0:
"""
This will execute when the tool page is initially loading and the
user has not yet manually selected a dataset. In this case, ad_val
will be a DataToolParameter.
"""
field = assoc_dataset.get_html_field(trans, assoc_dataset, other_values )
"""We need to make sure there is a dataset of the proper format in the history."""
some_data = bool( field.options )
if not some_data:
return columnList
elif len( field.options ) == 1 and field.options[0][0] == 'no data has the proper type':
return columnList
if dataset_id == 0:
for txt, val, selected in field.options:
if selected:
dataset_id = val
break
if dataset_id == 0:
"""This should be the latest dataset in the history"""
dataset_id = str(field.options[-1][1])
dataset = assoc_dataset.from_html(dataset_id, trans, other_values )
else:
"""
This will execute whenever the user selects a dataset from the input data
select list. Doing this results in executing the tool's update_state function,
so we can retrieve the selected dataset from the tool.
"""
dataset = self.tool.updated_params[self.assoc_dataset]
assert dataset is not None, "Error retrieving required dataset for ColumnListParameter"
"""Just to be safe..."""
self.data_ref = elem.get( "data_ref", None )
if self.data_ref is None:
self.data_ref = elem.get( "assoc_dataset", None )
def get_column_list( self, trans, other_values ):
"""
Generate a select list containing the columns of the associated
dataset (if found).
"""
column_list = []
# No value indicates a configuration error, the named DataToolParameter
# must preceed this parameter in the config
assert self.data_ref in other_values, "Value for associated DataToolParameter not found"
# Get the value of the associated DataToolParameter (a dataset)
dataset = other_values[ self.data_ref ]
# Check if a dataset is selected
if dataset is None or dataset == '':
# NOTE: Both of these values indicate that no dataset is selected.
# However, 'None' indicates that the dataset is optional
# while '' indicates that it is not. Currently column
# parameters do not work well with optional datasets
return column_list
# Just to be safe... (FIXME: Is this still neccesary?)
dataset.set_meta()
# Generate options
if self.numerical:
# If numerical was requsted, filter columns based on metadata
for i, col in enumerate( dataset.metadata.column_types ):
if col == 'int' or col == 'float':
self.legal_values.add(str( i+1 ))
option = 'c' + str( i+1 )
columnList.append((option,str( i+1 ),False))
column_list.append( str( i + 1 ) )
else:
for col in range (0, dataset.metadata.columns):
self.legal_values.add(str(col+1))
option = 'c' + str(col+1)
columnList.append((option,str(col+1),False))
return columnList
column_list = [ str( i + 1 ) for i in range(0, dataset.metadata.columns) ]
return column_list
def get_options( self, trans, other_values ):
column_list = self.get_column_list( trans, other_values )
return [ ( "c" + col, col, False ) for col in column_list ]
def get_legal_values( self, trans, other_values ):
return set( self.get_column_list( trans, other_values ) )
def get_dependencies( self ):
return [ self.data_ref ]
class DataToolParameter( ToolParameter ):
"""
@@ -655,11 +647,11 @@ class DataToolParameter( ToolParameter ):
if type( value ) != list: value = [ value ]
field = form_builder.SelectField( self.name, self.multiple, None, self.refresh_on_change )
if self.dynamic_options:
"""Dynamic options for a DataToolParameter specify limits on acceptrable build, id, or extension"""
# Dynamic options for a DataToolParameter specify limits on acceptrable build, id, or extension
option_build, option_id, option_extension = \
eval( self.dynamic_options, self.tool.code_namespace, other_values )
"""CRUCIAL: the dataset_collector function needs to be local to DataToolParameter.get_html_field()"""
def dataset_collector( datasets, parent_hid ):
# CRUCIAL: the dataset_collector function needs to be local to DataToolParameter.get_html_field()
def dataset_collector( datasets, parent_hid ):
for i, data in enumerate( datasets ):
if parent_hid is not None:
hid = "%s.%d" % ( parent_hid, i + 1 )
@@ -678,7 +670,6 @@ class DataToolParameter( ToolParameter ):
field.add_option( "%s: %s" % ( hid, data.name[:30] ), data.id, selected )
# Also collect children via association object
dataset_collector( [ assoc.child for assoc in data.children ], hid )
dataset_collector( history.datasets, None )
some_data = bool( field.options )
if some_data:
@@ -692,6 +683,41 @@ class DataToolParameter( ToolParameter ):
field.add_option( "Selection is Optional", 'None', True )
return field
def get_initial_value( self, trans, context ):
"""
NOTE: This is wasteful since dynamic options and dataset collection
happens twice (here and when generating HTML).
"""
assert trans is not None, "DataToolParameter requires a trans"
history = trans.history
assert history is not None, "DataToolParameter requires a history"
if self.dynamic_options:
# Dynamic options for a DataToolParameter specify limits on acceptrable build, id, or extension
option_build, option_id, option_extension = \
eval( self.dynamic_options, self.tool.code_namespace, other_values )
most_recent_dataset = [None]
def dataset_collector( datasets ):
for i, data in enumerate( datasets ):
if self.dynamic_options:
if ( isinstance( data.datatype, self.formats )
and (data.dbkey == option_build) and (data.id != option_id)
and (data.extension in option_extension)
and not data.deleted ):
most_recent_dataset[0] = data
else:
if isinstance( data.datatype, self.formats) and not data.deleted:
most_recent_dataset[0] = data
# Also collect children via association object
dataset_collector( [ assoc.child for assoc in data.children ] )
dataset_collector( history.datasets )
most_recent_dataset = most_recent_dataset.pop()
if most_recent_dataset is not None:
return most_recent_dataset
elif self.optional:
return None
else:
return ''
def from_html( self, value, trans, other_values={} ):
if not value:
raise ValueError( "A data of the appropriate type is required" )
@@ -724,6 +750,9 @@ class DataToolParameter( ToolParameter ):
def to_param_dict_string( self, value ):
return value.file_name
def value_to_display_text( self, value, app ):
return "%s: %s" % ( value.hid, value.name )
# class RawToolParameter( ToolParameter ):
# """
@@ -781,6 +810,7 @@ parameter_types = dict( text = TextToolParameter,
genomebuild = GenomeBuildParameter,
select = SelectToolParameter,
columnlist = ColumnListParameter,
data_column = ColumnListParameter,
hidden = HiddenToolParameter,
baseurl = BaseURLToolParameter,
file = FileToolParameter,
+4 -9
View File
@@ -44,20 +44,15 @@
#set group_errors = $errors.get( $input.name, {} )
#set current_case = $group_state['__current_case__']
#set prefix = $prefix + $input.name + "|"
$row_for_param( $prefix, $input.test_param, $group_state, $group_errors, $context, refresh=True )
$row_for_param( $prefix, $input.test_param, $group_state, $group_errors, $context )
$do_inputs( $input.cases[$current_case].inputs, $group_state, $group_errors, $prefix, $context )
#else
#try
#set refresh = $input.refresh_on_change
#except
#set refresh = False
#end try
$row_for_param( $prefix, $input, $tool_state, $errors, $context, $refresh )
$row_for_param( $prefix, $input, $tool_state, $errors, $context )
#end if
#end for
#end def
#def row_for_param( $prefix, $param, $parent_state, $parent_errors, $context, $refresh=False )
#def row_for_param( $prefix, $param, $parent_state, $parent_errors, $context )
#if $parent_errors.has_key( $param.name ):
#set cls = "form-row form-row-error"
#else
@@ -69,7 +64,7 @@
</label>
#set field = $param.get_html_field( $caller, $parent_state[ $param.name ], $context )
#set $field.refresh_on_change = $refresh
#set $field.refresh_on_change = $param.refresh_on_change
<div style="float: left; width: 250px; margin-right: 10px;">$field.get_html( $prefix )</div>
#if $parent_errors.has_key( $param.name ):
<div style="float: left; color: red; font-weight: bold; padding-top: 1px; padding-bottom: 3px;">
+2 -2
View File
@@ -18,10 +18,10 @@
<param name="input"
type="data" format="tabular"
label="Dataset"/>
<param name="xcol" type="integer"
<param name="xcol" type="data_column" data_ref="input"
value="1" size="30"
label="Column for x axis"/>
<param name="ycol" type="integer"
<param name="ycol" type="data_column" data_ref="input"
value="1" size="30"
label="Column for y axis"/>
<conditional name="series_type">