Visualizations framework: allow users to associate custom visualizations with models, datatypes, etc. via an xml configuration file (visualizations_conf.xml)

This commit is contained in:
Carl Eberhard
2013-05-17 14:45:07 -04:00
parent 936df8d89e
commit df1927422c
19 changed files with 1630 additions and 50 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ shed_tool_data_table_conf.xml
job_conf.xml
data_manager_conf.xml
shed_data_manager_conf.xml
visualizations_conf.xml
static/welcome.html.*
static/welcome.html
+4
View File
@@ -15,6 +15,7 @@ import galaxy.quota
from galaxy.tags.tag_handler import GalaxyTagHandler
from galaxy.visualization.genomes import Genomes
from galaxy.visualization.data_providers.registry import DataProviderRegistry
from galaxy.visualization.registry import VisualizationsRegistry
from galaxy.tools.imp_exp import load_history_imp_exp_tools
from galaxy.tools.genome_index import load_genome_index_tools
from galaxy.sample_tracking import external_service_types
@@ -120,6 +121,9 @@ class UniverseApplication( object ):
load_history_imp_exp_tools( self.toolbox )
# Load genome indexer tool.
load_genome_index_tools( self.toolbox )
# visualizations registry: associates resources with visualizations, controls how to render
self.visualizations_registry = ( VisualizationsRegistry( self.config.root, self.config.visualizations_conf_path )
if self.config.visualizations_conf_path else None )
# Load security policy.
self.security_agent = self.model.security_agent
self.host_security_agent = galaxy.security.HostAgent( model=self.security_agent.model, permitted_actions=self.security_agent.permitted_actions )
+2
View File
@@ -275,6 +275,8 @@ class Configuration( object ):
self.fluent_log = string_as_bool( kwargs.get( 'fluent_log', False ) )
self.fluent_host = kwargs.get( 'fluent_host', 'localhost' )
self.fluent_port = int( kwargs.get( 'fluent_port', 24224 ) )
# visualizations registry config path
self.visualizations_conf_path = kwargs.get( 'visualizations_conf_path', None )
@property
def sentry_dsn_public( self ):
+30
View File
@@ -379,6 +379,36 @@ class Registry( object ):
if not included:
self.sniff_order.append(datatype)
append_to_sniff_order()
def get_datatype_class_by_name( self, name ):
"""
Return the datatype class where the datatype's `type` attribute
(as defined in the datatype_conf.xml file) contains `name`.
"""
#TODO: too roundabout - would be better to generate this once as a map and store in this object
found_class = None
for ext, datatype_obj in self.datatypes_by_extension.items():
datatype_obj_class = datatype_obj.__class__
datatype_obj_class_str = str( datatype_obj_class )
#print datatype_obj_class_str
if name in datatype_obj_class_str:
return datatype_obj_class
return None
# these seem to be connected to the dynamic classes being generated in this file, lines 157-158
# they appear when a one of the three are used in inheritance with subclass="True"
#TODO: a possible solution is to def a fn in datatypes __init__ for creating the dynamic classes
#remap = {
# 'galaxy.datatypes.registry.Tabular' : galaxy.datatypes.tabular.Tabular,
# 'galaxy.datatypes.registry.Text' : galaxy.datatypes.data.Text,
# 'galaxy.datatypes.registry.Binary' : galaxy.datatypes.binary.Binary
#}
#datatype_str = str( datatype )
#if datatype_str in remap:
# datatype = remap[ datatype_str ]
#
#return datatype
def get_available_tracks(self):
return self.available_tracks
def get_mimetype_by_extension(self, ext, default = 'application/octet-stream' ):
@@ -30,7 +30,9 @@ class DataProviderRegistry( object ):
"bai": genome.BamDataProvider,
"bam": genome.SamDataProvider,
"bigwig": genome.BigWigDataProvider,
"bigbed": genome.BigBedDataProvider
"bigbed": genome.BigBedDataProvider,
"column": ColumnDataProvider
}
def get_data_provider( self, trans, name=None, source='data', raw=False, original_dataset=None ):
@@ -105,4 +107,4 @@ class DataProviderRegistry( object ):
except NoConverterException:
pass
return data_provider
return data_provider
+808
View File
@@ -0,0 +1,808 @@
"""
Lower level of visualization framework which does three main things:
- associate visualizations with objects
- create urls to visualizations based on some target object(s)
- unpack a query string into the desired objects needed for rendering
"""
import os
import shutil
from galaxy import util
import galaxy.model
from galaxy.web import url_for
import logging
log = logging.getLogger( __name__ )
__TODO__ = """
BUGS:
anon users clicking a viz link gets 'must be' msg in galaxy_main (w/ masthead)
should not show visualizations (no icon)?
newick files aren't being sniffed prop? - datatype is txt
have parsers create objects instead of dicts
allow data_sources with no model_class but have tests (isAdmin, etc.)
maybe that's an instance of User model_class?
some confused vocabulary in docs, var names
tests:
anding, grouping, not
data_sources:
lists of
add description element to visualization.
"""
class VisualizationsRegistry( object ):
"""
Main responsibilities are:
- testing if an object has a visualization that can be applied to it
- generating a link to controllers.visualization.render with
the appropriate params
- validating and parsing params into resources (based on a context)
used in the visualization template
"""
# these should be handled somewhat differently - and be passed onto their resp. methods in ctrl.visualization
#TODO: change/remove if/when they can be updated to use this system
BUILT_IN_VISUALIZATIONS = [
'trackster',
'circster',
'sweepster',
'phyloviz'
]
# where to search for visualiztion templates (relative to templates/webapps/galaxy)
# this can be overridden individually in the config entries
TEMPLATE_ROOT = 'visualization'
def __str__( self ):
listings_keys_str = ','.join( self.listings.keys() ) if self.listings else ''
return 'VisualizationsRegistry(%s)' %( listings_keys_str )
def __init__( self, galaxy_root, configuration_filepath ):
# load the registry from the given xml file using the given parser
configuration_filepath = os.path.join( galaxy_root, configuration_filepath )
configuration_filepath = self.check_conf_filepath( configuration_filepath )
self.configuration_filepath = configuration_filepath
self.load()
# what to use to parse query strings into resources/vars for the template
self.resource_parser = ResourceParser()
def check_conf_filepath( self, configuration_filepath ):
"""
If given file at filepath exists, return that filepath. If not,
see if filepath + '.sample' exists and, if so, copy that into filepath.
If neither original or sample exist, throw an IOError (currently,
this is a requireed file).
"""
if os.path.exists( configuration_filepath ):
return configuration_filepath
else:
sample_file = configuration_filepath + '.sample'
if os.path.exists( sample_file ):
shutil.copy2( sample_file, configuration_filepath )
return configuration_filepath
raise IOError( 'visualization configuration file (%s) not found' %( configuration_filepath ) )
def load( self ):
"""
Builds the registry by parsing the xml in `self.configuration_filepath`
and stores the results in `self.listings`.
Provided as separate method from `__init__` in order to re-load a
new configuration without restarting the instance.
"""
self.listings = VisualizationsConfigParser.parse( self.configuration_filepath )
# -- building links to visualizations from objects --
def get_visualizations( self, trans, target_object ):
"""
Get the names of visualizations usable on the `target_object` and
the urls to call in order to render the visualizations.
"""
#TODO:?? a list of objects? YAGNI?
# a little weird to pass trans because this registry is part of the trans.app
applicable_visualizations = []
for vis_name, listing_data in self.listings.items():
data_sources = listing_data[ 'data_sources' ]
for data_source in data_sources:
# currently a model class is required
model_class = data_source[ 'model_class' ]
if not isinstance( target_object, model_class ):
continue
# tests are optional - default is the above class test
tests = data_source[ 'tests' ]
if tests and not self.is_object_applicable( trans, target_object, tests ):
continue
param_data = data_source[ 'to_params' ]
url = self.get_visualization_url( trans, target_object, vis_name, param_data )
link_text = listing_data.get( 'link_text', None )
if not link_text:
# default to visualization name, titlecase, and replace underscores
link_text = vis_name.title().replace( '_', ' ' )
render_location = listing_data.get( 'render_location' )
# remap some of these vars for direct use in ui.js, PopupMenu (e.g. text->html)
applicable_visualizations.append({
'href' : url,
'html' : link_text,
'target': render_location
})
return applicable_visualizations
def is_object_applicable( self, trans, target_object, data_source_tests ):
"""
Run a visualization's data_source tests to find out if
it be applied to the target_object.
"""
for test in data_source_tests:
test_type = test[ 'type' ]
result_type = test[ 'result_type' ]
test_result = test[ 'result' ]
test_fn = test[ 'fn' ]
#log.debug( '%s %s: %s, %s, %s, %s', str( target_object ), 'is_object_applicable',
# test_type, result_type, test_result, test_fn )
if test_type == 'isinstance':
# parse test_result based on result_type (curr: only datatype has to do this)
if result_type == 'datatype':
# convert datatypes to their actual classes (for use with isinstance)
test_result = trans.app.datatypes_registry.get_datatype_class_by_name( test_result )
if not test_result:
# warn if can't find class, but continue
log.warn( 'visualizations_registry cannot find class (%s) for applicability test', test_result )
continue
if test_fn( target_object, test_result ):
#log.debug( 'test passed' )
return True
return False
def get_visualization_url( self, trans, target_object, visualization_name, param_data ):
"""
Generates a url for the visualization with `visualization_name`
for use with the given `target_object` with a query string built
from the configuration data in `param_data`.
"""
#precondition: the target_object should be usable by the visualization (accrd. to data_sources)
# convert params using vis.data_source.to_params
params = self.get_url_params( trans, target_object, param_data )
# we want existing visualizations to work as normal but still be part of the registry (without mod'ing)
# so generate their urls differently
url = None
if visualization_name in self.BUILT_IN_VISUALIZATIONS:
url = url_for( controller='visualization', action=visualization_name, **params )
else:
url = url_for( controller='visualization', action='render',
visualization_name=visualization_name, **params )
#TODO:?? not sure if embedded would fit/used here? or added in client...
return url
def get_url_params( self, trans, target_object, param_data ):
"""
Convert the applicable objects and assoc. data into a param dict
for a url query string to add to the url that loads the visualization.
"""
params = {}
for to_param_name, to_param_data in param_data.items():
#TODO??: look into params as well? what is required, etc.
target_attr = to_param_data.get( 'param_attr', None )
assign = to_param_data.get( 'assign', None )
# one or the other is needed
# assign takes precedence (goes last, overwrites)?
#NOTE this is only one level
if target_attr and hasattr( target_object, target_attr ):
params[ to_param_name ] = getattr( target_object, target_attr )
if assign:
params[ to_param_name ] = assign
#NOTE!: don't expose raw ids: encode id, _id
if params:
params = trans.security.encode_dict_ids( params )
return params
# -- getting resources for visualization templates from link query strings --
def get_resource_params_and_modifiers( self, visualization_name ):
"""
Get params and modifiers for the given visualization as a 2-tuple.
Both `params` and `param_modifiers` default to an empty dictionary.
"""
visualization = self.listings.get( visualization_name )
expected_params = visualization.get( 'params', {} )
param_modifiers = visualization.get( 'param_modifiers', {} )
return ( expected_params, param_modifiers )
def query_dict_to_resources( self, trans, controller, visualization_name, query_dict ):
"""
Use a resource parser, controller, and a visualization's param configuration
to convert a query string into the resources and variables a visualization
template needs to start up.
"""
param_confs, param_modifiers = self.get_resource_params_and_modifiers( visualization_name )
resources = self.resource_parser.parse_parameter_dictionary(
trans, controller, param_confs, query_dict, param_modifiers )
return resources
# ------------------------------------------------------------------- parsing the config file
class ParsingException( ValueError ):
"""
An exception class for errors that occur during parsing of the visualizations
framework configuration XML file.
"""
pass
class VisualizationsConfigParser( object ):
"""
Class that parses a visualizations configuration XML file.
Each visualization will get the following info:
- how to load a visualization:
-- how to find the proper template
-- how to convert query string into DB models
- when/how to generate a link to the visualization
-- what provides the data
-- what information needs to be added to the query string
"""
VALID_RENDER_LOCATIONS = [ 'galaxy_main', '_top', '_blank' ]
@classmethod
def parse( cls, xml_filepath, debug=True ):
"""
Static class interface
"""
return cls( debug ).parse_file( xml_filepath )
def __init__( self, debug=False ):
self.debug = debug
# what parsers should be used for sub-components
self.data_source_parser = DataSourceParser()
self.param_parser = ParamParser()
self.param_modifier_parser = ParamModifierParser()
def parse_file( self, xml_filepath ):
"""
Parse the given XML file for visualizations data.
If an error occurs while parsing a visualizations entry it is skipped.
"""
returned = {}
try:
xml_tree = galaxy.util.parse_xml( xml_filepath )
for visualization_conf in xml_tree.getroot().findall( 'visualization' ):
visualization = None
visualization_name = visualization_conf.get( 'name' )
try:
visualization = self.parse_visualization( visualization_conf )
# skip vis' with parsing errors - don't shutdown the startup
except ParsingException, parse_exc:
log.error( 'Skipped visualization configuration "%s" due to parsing errors: %s',
visualization_name, str( parse_exc ), exc_info=self.debug )
if visualization:
returned[ visualization_name ] = visualization
except Exception, exc:
log.error( 'Error parsing visualization configuration file %s: %s',
xml_filepath, str( exc ), exc_info=( not self.debug ) )
#TODO: change when this is required
if self.debug:
raise
return returned
def parse_visualization( self, xml_tree ):
"""
Parse the template, name, and any data_sources and params from the
given `xml_tree` for a visualization.
"""
returned = {}
# data_sources are the kinds of objects/data associated with the visualization
# e.g. views on HDAs can use this to find out what visualizations are applicable to them
data_sources = []
data_sources_confs = xml_tree.find( 'data_sources' )
for data_source_conf in data_sources_confs.findall( 'data_source' ):
data_source = self.data_source_parser.parse( data_source_conf )
if data_source:
data_sources.append( data_source )
# data_sources are not required
if not data_sources:
raise ParsingException( 'No valid data_sources for visualization' )
returned[ 'data_sources' ] = data_sources
# parameters spell out how to convert query string params into resources and data
# that will be parsed, fetched, etc. and passed to the template
# list or dict? ordered or not?
params = {}
param_confs = xml_tree.find( 'params' )
for param_conf in param_confs.findall( 'param' ):
param = self.param_parser.parse( param_conf )
if param:
params[ param_conf.text ]= param
# params are not required
if params:
returned[ 'params' ] = params
# param modifiers provide extra information for other params (e.g. hda_ldda='hda' -> dataset_id is an hda id)
# store these modifiers in a 2-level dictionary { target_param: { param_modifier_key: { param_mod_data }
# ugh - wish we didn't need these
param_modifiers = {}
for param_modifier_conf in param_confs.findall( 'param_modifier' ):
param_modifier = self.param_modifier_parser.parse( param_modifier_conf )
# param modifiers map accrd. to the params they modify (for faster lookup)
target_param = param_modifier_conf.get( 'modifies' )
param_modifier_key = param_modifier_conf.text
if param_modifier and target_param in params:
# multiple params can modify a single, other param,
# so store in a sub-dict, initializing if this is the first
if target_param not in param_modifiers:
param_modifiers[ target_param ] = {}
param_modifiers[ target_param ][ param_modifier_key ] = param_modifier
# not required
if param_modifiers:
returned[ 'param_modifiers' ] = param_modifiers
# the template to use in rendering the visualization (required)
template = xml_tree.find( 'template' )
if template == None or not template.text:
raise ParsingException( 'template filename required' )
returned[ 'template' ] = template.text
# link_text: the string to use for the text of any links/anchors to this visualization
link_text = xml_tree.find( 'link_text' )
if link_text != None and link_text.text:
returned[ 'link_text' ] = link_text
# render_location: where in the browser to open the rendered visualization
# defaults to: galaxy_main
render_location = xml_tree.find( 'render_location' )
if( ( render_location != None and render_location.text )
and ( render_location.text in self.VALID_RENDER_LOCATIONS ) ):
returned[ 'render_location' ] = render_location.text
else:
returned[ 'render_location' ] = 'galaxy_main'
# consider unifying the above into it's own element and parsing method
return returned
# ------------------------------------------------------------------- parsing a query string into resources
class DataSourceParser( object ):
"""
Component class of VisualizationsConfigParser that parses data_source elements
within visualization elements.
data_sources are (in the extreme) any object that can be used to produce
data for the visualization to consume (e.g. HDAs, LDDAs, Jobs, Users, etc.).
There can be more than one data_source associated with a visualization.
"""
# these are the allowed classes to associate visualizations with (as strings)
# any model_class element not in this list will throw a parsing ParsingExcepion
ALLOWED_MODEL_CLASSES = [
'HistoryDatasetAssociation',
'LibraryDatasetDatasetAssociation'
]
ATTRIBUTE_SPLIT_CHAR = '.'
# these are the allowed object attributes to use in data source tests
# any attribute element not in this list will throw a parsing ParsingExcepion
ALLOWED_DATA_SOURCE_ATTRIBUTES = [
'datatype'
]
def parse( self, xml_tree ):
"""
Return a visualization data_source dictionary parsed from the given
XML element.
"""
returned = {}
# model_class (required, only one) - look up and convert model_class to actual galaxy model class
model_class = self.parse_model_class( xml_tree.find( 'model_class' ) )
if not model_class:
raise ParsingException( 'data_source needs a model class' )
returned[ 'model_class' ] = model_class
# tests (optional, 0 or more) - data for boolean test: 'is the visualization usable by this object?'
tests = self.parse_tests( xml_tree.findall( 'test' ) )
# when no tests are given, default to isinstance( object, model_class )
if tests:
returned[ 'tests' ] = tests
# to_params (optional, 0 or more) - tells the registry to set certain params based on the model_clas, tests
to_params = self.parse_to_params( xml_tree.findall( 'to_param' ) )
if to_params:
returned[ 'to_params' ] = to_params
return returned
def parse_model_class( self, xml_tree ):
"""
Convert xml model_class element to a galaxy model class
(or None if model class is not found).
This element is required and only the first element is used.
The model_class string must be in ALLOWED_MODEL_CLASSES.
"""
if xml_tree is None or not xml_tree.text:
raise ParsingException( 'data_source entry requires a model_class' )
if xml_tree.text not in self.ALLOWED_MODEL_CLASSES:
log.debug( 'available data_source model_classes: %s' %( str( self.ALLOWED_MODEL_CLASSES ) ) )
raise ParsingException( 'Invalid data_source model_class: %s' %( xml_tree.text ) )
# look up the model from the model module returning an empty data_source if not found
model_class = getattr( galaxy.model, xml_tree.text, None )
return model_class
def _build_getattr_lambda( self, attr_name_list ):
"""
Recursively builds a compound lambda function of getattr's
from the attribute names given in `attr_name_list`.
"""
if len( attr_name_list ) == 0:
# identity - if list is empty, return object itself
return lambda o: o
next_attr_name = attr_name_list[-1]
if len( attr_name_list ) == 1:
# recursive base case
return lambda o: getattr( o, next_attr_name )
# recursive case
return lambda o: getattr( self._build_getattr_lambda( attr_name_list[:-1] ), next_attr_name )
def parse_tests( self, xml_tree_list ):
"""
Returns a list of test dictionaries that the registry can use
against a given object to determine if the visualization can be
used with the object.
"""
# tests should NOT include expensive operations: reading file data, running jobs, etc.
# do as much here as possible to reduce the overhead of seeing if a visualization is applicable
# currently tests are or'd only (could be and'd or made into compound boolean tests)
tests = []
if not xml_tree_list:
return tests
for test_elem in xml_tree_list:
test_type = test_elem.get( 'type' )
test_result = test_elem.text
if not test_type or not test_result:
log.warn( 'Skipping test. Needs both type attribute and text node to be parsed: '
+ '%s, %s' %( test_type, test_elem.text ) )
continue
# test_attr can be a dot separated chain of object attributes (e.g. dataset.datatype) - convert to list
#TODO: too dangerous - constrain these to some allowed list
test_attr = test_elem.get( 'test_attr' )
test_attr = test_attr.split( self.ATTRIBUTE_SPLIT_CHAR ) if isinstance( test_attr, str ) else []
# build a lambda function that gets the desired attribute to test
getter = self._build_getattr_lambda( test_attr )
# result type should tell the registry how to convert the result before the test
test_result_type = test_elem.get( 'result_type' ) or 'string'
# test functions should be sent an object to test, and the parsed result expected from the test
#TODO: currently, isinstance and string equivalance are the only test types supported
if test_type == 'isinstance':
#TODO: wish we could take this further but it would mean passing in the datatypes_registry
test_fn = lambda o, result: isinstance( getter( o ), result )
# default to simple (string) equilavance (coercing the test_attr to a string)
else:
test_fn = lambda o, result: str( getter( o ) ) == result
tests.append({
'type' : test_type,
'result' : test_result,
'result_type' : test_result_type,
'fn' : test_fn
})
return tests
def parse_to_params( self, xml_tree_list ):
"""
Given a list of `to_param` elements, returns a dictionary that allows
the registry to convert the data_source into one or more appropriate
params for the visualization.
"""
to_param_dict = {}
if not xml_tree_list:
return to_param_dict
for element in xml_tree_list:
# param_name required
param_name = element.text
if not param_name:
raise ParsingException( 'to_param requires text (the param name)' )
param = {}
# assign is a shortcut param_attr that assigns a value to a param (as text)
assign = element.get( 'assign' )
if assign != None:
param[ 'assign' ] = assign
# param_attr is the attribute of the object (that the visualization will be applied to)
# that should be converted into a query param (e.g. param_attr="id" -> dataset_id)
#TODO:?? use the build attr getter here?
# simple (1 lvl) attrs for now
param_attr = element.get( 'param_attr' )
if param_attr != None:
param[ 'param_attr' ] = param_attr
# element must have either param_attr or assign? what about no params (the object itself)
if not param_attr and not assign:
raise ParsingException( 'to_param requires either assign or param_attr attributes: %s', param_name )
#TODO: consider making the to_param name an attribute (param="hda_ldda") and the text what would
# be used for the conversion - this would allow CDATA values to be passed
#<to_param param="json" type="assign"><![CDATA[{ "one": 1, "two": 2 }]]></to_param>
if param:
to_param_dict[ param_name ] = param
return to_param_dict
class ParamParser( object ):
"""
Component class of VisualizationsConfigParser that parses param elements
within visualization elements.
params are parameters that will be parsed (based on their `type`, etc.)
and sent to the visualization template by controllers.visualization.render.
"""
DEFAULT_PARAM_TYPE = 'str'
def parse( self, xml_tree ):
"""
Parse a visualization parameter from the given `xml_tree`.
"""
returned = {}
# don't store key, just check it
param_key = xml_tree.text
if not param_key:
raise ParsingException( 'Param entry requires text' )
returned[ 'type' ] = self.parse_param_type( xml_tree )
# is the parameter required in the template and,
# if not, what is the default value?
required = xml_tree.get( 'required' ) == "true"
returned[ 'required' ] = required
if not required:
# default defaults to None
default = None
if 'default' in xml_tree.attrib:
default = xml_tree.get( 'default' )
# convert default based on param_type here
returned[ 'default' ] = default
# does the param have to be within a list of certain values
# NOTE: the interpretation of this list is deferred till parsing and based on param type
# e.g. it could be 'val in constrain_to', or 'constrain_to is min, max for number', etc.
#TODO: currently unused
constrain_to = xml_tree.get( 'constrain_to' )
if constrain_to:
returned[ 'constrain_to' ] = constrain_to.split( ',' )
# is the param a comma-separated-value list?
returned[ 'csv' ] = xml_tree.get( 'csv' ) == "true"
# remap keys in the params/query string to the var names used in the template
var_name_in_template = xml_tree.get( 'var_name_in_template' )
if var_name_in_template:
returned[ 'var_name_in_template' ] = var_name_in_template
return returned
def parse_param_type( self, xml_tree ):
"""
Parse a param type from the given `xml_tree`.
"""
# default to string as param_type
param_type = xml_tree.get( 'type' ) or self.DEFAULT_PARAM_TYPE
#TODO: set parsers and validaters, convert here
return param_type
class ParamModifierParser( ParamParser ):
"""
Component class of VisualizationsConfigParser that parses param_modifier
elements within visualization elements.
param_modifiers are params from a dictionary (such as a query string)
that are not standalone but modify the parsing/conversion of a separate
(normal) param (e.g. 'hda_ldda' can equal 'hda' or 'ldda' and control
whether a visualizations 'dataset_id' param is for an HDA or LDDA).
"""
def parse( self, element ):
# modifies is required
modifies = element.get( 'modifies' )
if not modifies:
raise ParsingException( 'param_modifier entry requires a target param key (attribute "modifies")' )
returned = super( ParamModifierParser, self).parse( element )
return returned
class ResourceParser( object ):
"""
Given a parameter dictionary (often a converted query string) and a
configuration dictionary (curr. only VisualizationsRegistry uses this),
convert the entries in the parameter dictionary into resources (Galaxy
models, primitive types, lists of either, etc.) and return
in a new dictionary.
The keys used to store the new values can optionally be re-mapped to
new keys (e.g. dataset_id="NNN" -> hda=<HistoryDatasetAsscoation>).
"""
#TODO: kinda torn as to whether this belongs here or in controllers.visualization
# taking the (questionable) design path of passing a controller in
# (which is the responsible party for getting model, etc. resources )
# consider making this a base controller? use get_object for the model resources
# don't like passing in the app, tho
def parse_parameter_dictionary( self, trans, controller, param_config_dict, query_params, param_modifiers=None ):
"""
Parse all expected params from the query dictionary `query_params`.
If param is required and not present, raises a `KeyError`.
"""
# parse the modifiers first since they modify the params coming next
#TODO: this is all really for hda_ldda - which we could replace with model polymorphism
params_that_modify_other_params = self.parse_parameter_modifiers(
trans, controller, param_modifiers, query_params )
resources = {}
for param_name, param_config in param_config_dict.items():
# optionally rename the variable returned, defaulting to the original name
var_name_in_template = param_config.get( 'var_name_in_template', param_name )
# if the param is present, get it's value, any param modifiers for that param, and parse it into a resource
# use try catch here and not caller to fall back on the default value or re-raise if required
resource = None
query_val = query_params.get( param_name, None )
if query_val is not None:
try:
target_param_modifiers = params_that_modify_other_params.get( param_name, None )
resource = self.parse_parameter( trans, controller, param_config,
query_val, param_modifiers=target_param_modifiers )
except Exception, exception:
log.warn( 'Exception parsing visualization param from query: '
+ '%s, %s, (%s) %s' %( param_name, query_val, str( type( exception ) ), str( exception ) ))
resource = None
# here - we've either had no value in the query_params or there was a failure to parse
# so: error if required, otherwise get a default (which itself defaults to None)
if resource == None:
if param_config[ 'required' ]:
raise KeyError( 'required param %s not found in URL' %( param_name ) )
resource = self.parse_parameter_default( trans, param_config )
resources[ var_name_in_template ] = resource
return resources
#TODO: I would LOVE to rip modifiers out completely
def parse_parameter_modifiers( self, trans, controller, param_modifiers, query_params ):
"""
Parse and return parameters that are meant to modify other parameters,
be grouped with them, or are needed to successfully parse other parameters.
"""
# only one level of modification - down that road lies madness
# parse the modifiers out of query_params first since they modify the other params coming next
parsed_modifiers = {}
if not param_modifiers:
return parsed_modifiers
#precondition: expects a two level dictionary
# { target_param_name -> { param_modifier_name -> { param_modifier_data }}}
for target_param_name, modifier_dict in param_modifiers.items():
parsed_modifiers[ target_param_name ] = target_modifiers = {}
for modifier_name, modifier_config in modifier_dict.items():
query_val = query_params.get( modifier_name, None )
if query_val is not None:
modifier = self.parse_parameter( trans, controller, modifier_config, query_val )
target_modifiers[ modifier_name ] = modifier
else:
#TODO: required attr?
target_modifiers[ modifier_name ] = self.parse_parameter_default( trans, modifier_config )
return parsed_modifiers
def parse_parameter_default( self, trans, param_config ):
"""
Parse any default values for the given param, defaulting the default
to `None`.
"""
# currently, *default* default is None, so this is quaranteed to be part of the dictionary
default = param_config[ 'default' ]
# if default is None, do not attempt to parse it
if default == None:
return default
# otherwise, parse (currently param_config['default'] is a string just like query param and needs to be parsed)
# this saves us the trouble of parsing the default when the config file is read
# (and adding this code to the xml parser)
return self.parse_parameter( trans, param_config, default )
def parse_parameter( self, trans, controller, expected_param_data, query_param,
recurse=True, param_modifiers=None ):
"""
Use data in `expected_param_data` to parse `query_param` from a string into
a resource usable directly by a template.
'Primitive' types (string, int, etc.) are parsed here and more complex
resources (such as ORM models) are parsed via the `controller` passed
in.
"""
param_type = expected_param_data.get( 'type' )
constrain_to = expected_param_data.get( 'constrain_to' )
csv = expected_param_data.get( 'csv' )
parsed_param = None
# handle recursion for csv values
if csv and recurse:
parsed_param = []
query_param_list = galaxy.util.listify( query_param )
for query_param in query_param_list:
parsed_param.append( self._parse_param( trans, expected_param_data, query_param, recurse=False ) )
return parsed_param
primitive_parsers = {
'str' : lambda param: galaxy.util.sanitize_html.sanitize_html( param, 'utf-8' ),
'bool' : lambda param: galaxy.util.string_as_bool( param ),
'int' : lambda param: int( param ),
'float' : lambda param: float( param ),
#'date' : lambda param: ,
'json' : ( lambda param: galaxy.util.json.from_json_string(
galaxy.util.sanitize_html.sanitize_html( param ) ) ),
}
parser = primitive_parsers.get( param_type, None )
if parser:
#TODO: what about param modifiers on primitives?
parsed_param = parser( query_param )
#TODO: constrain_to
# this gets complicated - for strings - relatively simple but still requires splitting and using in
# for more complicated cases (ints, json) this gets weird quick
#TODO:?? remove?
# db models
#TODO: subclass here?
elif param_type == 'visualization':
encoded_visualization_id = query_param
#TODO:?? some fallback if there's no get_X in controller that's passed?
parsed_param = controller.get_visualization( trans, encoded_visualization_id,
check_ownership=False, check_accessible=True )
elif param_type == 'dataset':
encoded_dataset_id = query_param
# really an hda...
parsed_param = controller.get_dataset( trans, encoded_dataset_id,
check_ownership=False, check_accessible=True )
elif param_type == 'hda_or_ldda':
encoded_dataset_id = query_param
# needs info from another param...
hda_ldda = param_modifiers.get( 'hda_ldda' )
parsed_param = controller.get_hda_or_ldda( trans, hda_ldda, encoded_dataset_id )
#TODO: ideally this would check v. a list of valid dbkeys
elif param_type == 'dbkey':
dbkey = query_param
parsed_param = galaxy.util.sanitize_html.sanitize_html( dbkey, 'utf-8' )
#print ( '%s, %s -> %s, %s' %( param_type, query_param, str( type( parsed_param ) ), parsed_param ) )
return parsed_param
+223 -26
View File
@@ -14,7 +14,7 @@ import routes
from sqlalchemy import func, and_, select
from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNotImplemented, HTTPRequestRangeNotSatisfiable
from galaxy import util, web
from galaxy import util, web, model
from gettext import gettext
from galaxy.datatypes.interval import ChromatinInteractions
from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException, MessageException
@@ -28,7 +28,6 @@ from galaxy.workflow.modules import module_factory
from galaxy.model.orm import eagerload, eagerload_all
from galaxy.datatypes.data import Text
from galaxy.datatypes.display_applications import util as da_util
from galaxy.datatypes.metadata import FileParameter
@@ -486,6 +485,25 @@ class UsesHistoryDatasetAssociationMixin:
error( "Please wait until this dataset finishes uploading before attempting to view it." )
return hda
def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ):
"""
Returns one or more datasets in a list.
If a dataset is not found or is inaccessible to trans.user,
add None in its place in the list.
"""
# precondtion: dataset_ids is a list of encoded id strings
hdas = []
for id in hda_ids:
hda = None
try:
hda = self.get_dataset( trans, id,
check_ownership=check_ownership, check_accesible=check_accesible, check_state=check_state )
except Exception, exception:
pass
hdas.append( hda )
return hdas
def get_data( self, dataset, preview=True ):
"""
Gets a dataset's data.
@@ -552,9 +570,13 @@ class UsesHistoryDatasetAssociationMixin:
if meta_files:
hda_dict[ 'meta_files' ] = meta_files
#hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda )
#hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda )
hda_dict[ 'visualizations' ] = hda.get_visualizations()
# currently, the viz reg is optional - handle on/off
if trans.app.visualizations_registry:
hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda )
else:
hda_dict[ 'visualizations' ] = hda.get_visualizations()
#TODO: it may also be wiser to remove from here and add as API call that loads the visualizations
# when the visualizations button is clicked (instead of preloading/pre-checking)
# ---- return here if deleted
if hda.deleted and not purged:
@@ -662,18 +684,187 @@ class UsesLibraryMixinItems( SharableItemSecurityMixin ):
return self.get_object( trans, id, 'LibraryDataset', check_ownership=False, check_accessible=check_accessible )
class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
UsesLibraryMixinItems ):
""" Mixin for controllers that use Visualization objects. """
class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ):
"""
Mixin for controllers that use Visualization objects.
"""
DEFAULT_ORDER_BY = [ model.Visualization.title ]
viz_types = [ "trackster" ]
def create_visualization( self, trans, type, title="Untitled Genome Vis", slug=None, dbkey=None, annotation=None, config={}, save=True ):
""" Create visualiation and first revision. """
def get_visualization( self, trans, id, check_ownership=True, check_accessible=False ):
"""
Get a Visualization from the database by id, verifying ownership.
"""
# Load workflow from database
try:
visualization = trans.sa_session.query( trans.model.Visualization ).get( trans.security.decode_id( id ) )
except TypeError:
visualization = None
if not visualization:
error( "Visualization not found" )
else:
return self.security_check( trans, visualization, check_ownership, check_accessible )
def get_visualizations_by_user( self, trans, user, order_by=None, query_only=False ):
"""
Return query or query results of visualizations filtered by a user.
Set `order_by` to a column or list of columns to change the order
returned. Defaults to `DEFAULT_ORDER_BY`.
Set `query_only` to return just the query for further filtering or
processing.
"""
if not order_by:
order_by = self.DEFAULT_ORDER_BY
if not isinstance( order_by, list ):
order_by = [ order_by ]
query = trans.sa_session.query( model.Visualization )
query = query.filter( model.Visualization.user == user )
if order_by:
query = query.order_by( *order_by )
if query_only:
return query
return query.all()
def get_visualizations_shared_with_user( self, trans, user, order_by=None, query_only=False ):
"""
Return query or query results for visualizations shared with the given user.
Set `order_by` to a column or list of columns to change the order
returned. Defaults to `DEFAULT_ORDER_BY`.
Set `query_only` to return just the query for further filtering or
processing.
"""
if not order_by:
order_by = self.DEFAULT_ORDER_BY
if not isinstance( order_by, list ):
order_by = [ order_by ]
query = trans.sa_session.query( model.Visualization ).join( model.VisualizationUserShareAssociation )
query = query.filter( model.VisualizationUserShareAssociation.user_id == user.id )
# remove duplicates when a user shares with themselves?
query = query.filter( model.Visualization.user_id != user.id )
if order_by:
query = query.order_by( *order_by )
if query_only:
return query
return query.all()
def get_published_visualizations( self, trans, exclude_user=None, order_by=None, query_only=False ):
"""
Return query or query results for published visualizations optionally excluding
the user in `exclude_user`.
Set `order_by` to a column or list of columns to change the order
returned. Defaults to `DEFAULT_ORDER_BY`.
Set `query_only` to return just the query for further filtering or
processing.
"""
if not order_by:
order_by = self.DEFAULT_ORDER_BY
if not isinstance( order_by, list ):
order_by = [ order_by ]
query = trans.sa_session.query( model.Visualization )
query = query.filter( model.Visualization.published == True )
if exclude_user:
query = query.filter( model.Visualization.user != exclude_user )
if order_by:
query = query.order_by( *order_by )
if query_only:
return query
return query.all()
#TODO: move into model (get_api_value)
def get_visualization_summary_dict( self, visualization ):
"""
Return a set of summary attributes for a visualization in dictionary form.
NOTE: that encoding ids isn't done here should happen at the caller level.
"""
#TODO: deleted
#TODO: importable
return {
'id' : visualization.id,
'title' : visualization.title,
'type' : visualization.type,
'dbkey' : visualization.dbkey,
}
def get_visualization_dict( self, visualization ):
"""
Return a set of detailed attributes for a visualization in dictionary form.
The visualization's latest_revision is returned in its own sub-dictionary.
NOTE: that encoding ids isn't done here should happen at the caller level.
"""
return {
'model_class': 'Visualization',
'id' : visualization.id,
'title' : visualization.title,
'type' : visualization.type,
'user_id' : visualization.user.id,
'dbkey' : visualization.dbkey,
'slug' : visualization.slug,
# dictify only the latest revision (allow older to be fetched elsewhere)
'latest_revision' : self.get_visualization_revision_dict( visualization.latest_revision ),
'revisions' : [ r.id for r in visualization.revisions ],
}
def get_visualization_revision_dict( self, revision ):
"""
Return a set of detailed attributes for a visualization in dictionary form.
NOTE: that encoding ids isn't done here should happen at the caller level.
"""
return {
'model_class': 'VisualizationRevision',
'id' : revision.id,
'visualization_id' : revision.visualization.id,
'title' : revision.title,
'dbkey' : revision.dbkey,
'config' : revision.config,
}
def import_visualization( self, trans, id, user=None ):
"""
Copy the visualization with the given id and associate the copy
with the given user (defaults to trans.user).
Raises `ItemAccessibilityException` if `user` is not passed and
the current user is anonymous, and if the visualization is not `importable`.
Raises `ItemDeletionException` if the visualization has been deleted.
"""
# default to trans.user, error if anon
if not user:
if not trans.user:
raise ItemAccessibilityException( "You must be logged in to import Galaxy visualizations" )
user = trans.user
# check accessibility
visualization = self.get_visualization( trans, id, check_ownership=False )
if not visualization.importable:
raise ItemAccessibilityException( "The owner of this visualization has disabled imports via this link." )
if visualization.deleted:
raise ItemDeletionException( "You can't import this visualization because it has been deleted." )
# copy vis and alter title
#TODO: need to handle custom db keys.
imported_visualization = visualization.copy( user=user, title="imported: " + visualization.title )
trans.sa_session.add( imported_visualization )
trans.sa_session.flush()
return imported_visualization
def create_visualization( self, trans, type, title="Untitled Genome Vis", slug=None,
dbkey=None, annotation=None, config={}, save=True ):
"""
Create visualiation and first revision.
"""
visualization = self._create_visualization( trans, title, type, dbkey, slug, annotation, save )
#TODO: handle this error structure better either in _create or here
if isinstance( visualization, dict ):
err_dict = visualization
raise ValueError( err_dict[ 'title_err' ] or err_dict[ 'slug_err' ] )
# Create and save first visualization revision
revision = trans.model.VisualizationRevision( visualization=visualization, title=title, config=config, dbkey=dbkey )
revision = trans.model.VisualizationRevision( visualization=visualization, title=title,
config=config, dbkey=dbkey )
visualization.latest_revision = revision
if save:
@@ -683,6 +874,21 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
return visualization
def add_visualization_revision( self, trans, visualization, config, title, dbkey ):
"""
Adds a new `VisualizationRevision` to the given `visualization` with
the given parameters and set its parent visualization's `latest_revision`
to the new revision.
"""
#precondition: only add new revision on owned vis's
#TODO:?? should we default title, dbkey, config? to which: visualization or latest_revision?
revision = trans.model.VisualizationRevision( visualization, title, dbkey, config )
visualization.latest_revision = revision
#TODO:?? does this automatically add revision to visualzation.revisions?
trans.sa_session.add( revision )
trans.sa_session.flush()
return revision
def save_visualization( self, trans, config, type, id=None, title=None, dbkey=None, slug=None, annotation=None ):
session = trans.sa_session
@@ -697,8 +903,10 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
# Create new VisualizationRevision that will be attached to the viz
vis_rev = trans.model.VisualizationRevision()
vis_rev.visualization = vis
vis_rev.title = vis.title
vis_rev.dbkey = dbkey
# do NOT alter the dbkey
vis_rev.dbkey = vis.dbkey
# do alter the title and config
vis_rev.title = title
# -- Validate config. --
@@ -760,18 +968,6 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
encoded_id = trans.security.encode_id( vis.id )
return { "vis_id": encoded_id, "url": url_for( controller='visualization', action=vis.type, id=encoded_id ) }
def get_visualization( self, trans, id, check_ownership=True, check_accessible=False ):
""" Get a Visualization from the database by id, verifying ownership. """
# Load workflow from database
try:
visualization = trans.sa_session.query( trans.model.Visualization ).get( trans.security.decode_id( id ) )
except TypeError:
visualization = None
if not visualization:
error( "Visualization not found" )
else:
return self.security_check( trans, visualization, check_ownership, check_accessible )
def get_visualization_config( self, trans, visualization ):
""" Returns a visualization's configuration. Only works for trackster visualizations right now. """
config = None
@@ -911,7 +1107,6 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
if title_err or slug_err:
return { 'title_err': title_err, 'slug_err': slug_err }
# Create visualization
visualization = trans.model.Visualization( user=user, title=title, dbkey=dbkey, type=type )
if slug:
@@ -920,6 +1115,8 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin,
self.create_item_slug( trans.sa_session, visualization )
if annotation:
annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
#TODO: if this is to stay in the mixin, UsesAnnotations should be added to the superclasses
# right now this is depending on the classes that include this mixin to have UsesAnnotations
self.add_item_annotation( trans.sa_session, trans.user, visualization, annotation )
if save:
+9 -2
View File
@@ -51,17 +51,20 @@ def app_factory( global_conf, **kwargs ):
webapp.add_route( '/async/:tool_id/:data_id/:data_secret', controller='async', action='index', tool_id=None, data_id=None, data_secret=None )
webapp.add_route( '/:controller/:action', action='index' )
webapp.add_route( '/:action', controller='root', action='index' )
# allow for subdirectories in extra_files_path
webapp.add_route( '/datasets/:dataset_id/display/{filename:.+?}', controller='dataset', action='display', dataset_id=None, filename=None)
webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None)
webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param', controller='dataset', action='display_application', dataset_id=None, user_id=None, app_name = None, link_name = None, app_action = None, action_param = None )
webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param',
controller='dataset', action='display_application', dataset_id=None, user_id=None,
app_name = None, link_name = None, app_action = None, action_param = None )
webapp.add_route( '/u/:username/d/:slug/:filename', controller='dataset', action='display_by_username_and_slug', filename=None )
webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' )
webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' )
webapp.add_route( '/u/:username/w/:slug', controller='workflow', action='display_by_username_and_slug' )
webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' )
webapp.add_route( '/search', controller='search', action='index' )
# Add the web API
webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app )
# The /folders section is experimental at this point:
@@ -144,6 +147,10 @@ def app_factory( global_conf, **kwargs ):
#webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) )
webapp.mapper.resource( 'search', 'search', path_prefix='/api' )
# visualizations registry generic template renderer
webapp.add_route( '/visualization/show/:visualization_name',
controller='visualization', action='render', visualization_name=None )
# "POST /api/workflows/import" => ``workflows.import_workflow()``.
# Defines a named route "import_workflow".
webapp.mapper.connect("import_workflow", "/api/workflows/upload", controller="workflows", action="import_new_workflow", conditions=dict(method=["POST"]))
@@ -1,6 +1,10 @@
from __future__ import absolute_import
import os
from sqlalchemy import desc, or_, and_
from paste.httpexceptions import HTTPNotFound
from galaxy import model, web
from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings
from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesVisualizationMixin
@@ -16,6 +20,9 @@ from galaxy.visualization.genomes import GenomeRegion
from .library import LibraryListGrid
import logging
log = logging.getLogger( __name__ )
#
# -- Grids --
#
@@ -688,6 +695,54 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
dbkeys=trans.app.genomes.get_dbkeys( trans, chrom_info=True ),
default_dbkey=kwargs.get("default_dbkey", None) )
@web.expose
@web.require_login( "use Galaxy visualizations", use_panels=True )
def render( self, trans, visualization_name, embedded=None, **kwargs ):
"""
Render the appropriate visualization template, parsing the `kwargs`
into appropriate variables and resources (such as ORM models)
based on this visualizations `param` data in visualizations_conf.xml.
URL: /visualization/show/{visualization_name}
"""
# validate name vs. registry
registry = trans.app.visualizations_registry
if not registry:
raise HTTPNotFound( 'No visualization registry (possibly disabled in universe_wsgi.ini)')
if visualization_name not in registry.listings:
raise HTTPNotFound( 'Unknown or invalid visualization: ' + visualization_name )
# or redirect to list?
registry_listing = registry.listings[ visualization_name ]
returned = None
try:
# convert query string to resources for template based on registry config
#NOTE: passing in controller to keep resource lookup within the controller's responsibilities
# (and not the ResourceParser)
resources = registry.query_dict_to_resources( trans, self, visualization_name, kwargs )
# look up template and render
template_root = registry_listing.get( 'template_root', registry.TEMPLATE_ROOT )
template = registry_listing[ 'template' ]
template_path = os.path.join( template_root, template )
#NOTE: passing *unparsed* kwargs as query_args
#NOTE: shared_vars is a dictionary for shared data in the template
# this feels hacky to me but it's what mako recommends:
# http://docs.makotemplates.org/en/latest/runtime.html
#TODO: embedded
returned = trans.fill_template( template_path, visualization_name=visualization_name,
embedded=embedded, query_args=kwargs, shared_vars={}, **resources )
except Exception, exception:
log.exception( 'error rendering visualization (%s): %s', visualization_name, str( exception ) )
if trans.debug: raise
returned = trans.show_error_message(
"There was an error rendering the visualization. " +
"Contact your Galaxy administrator if the problem persists." +
"<br/>Details: " + str( exception ), use_panels=False )
return returned
@web.expose
@web.require_login()
def trackster(self, trans, id=None, **kwargs):
@@ -792,6 +847,8 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
get the visualization with the given id; otherwise, create a new visualization using
a given dataset and regions.
"""
print 'sweepster:', id, hda_ldda, dataset_id, regions
regions = regions or '{}'
# Need to create history if necessary in order to create tool form.
trans.get_history( create=True )
@@ -803,6 +860,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations,
else:
# Loading new visualization.
dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id )
print 'dataset:', dataset
job = get_dataset_job( dataset )
viz_config = {
'dataset_id': dataset_id,
+53 -9
View File
@@ -239,8 +239,25 @@ var HDAEditView = HDABaseView.extend( LoggableMixin ).extend(
* @returns {jQuery} rendered DOM
*/
_render_visualizationsButton : function(){
var visualizations = this.model.get( 'visualizations' );
if( ( !this.model.hasData() )
|| ( _.isEmpty( visualizations ) ) ){
this.visualizationsButton = null;
return null;
}
//TODO: this is a bridge to allow the framework to be switched off
// remove this fn and use the other when fully integrated
if( _.isObject( visualizations[0] ) ){
return this._render_visualizationsFrameworkButton( visualizations );
}
if( !this.urls.visualization ){
this.visualizationsButton = null;
return null;
}
var dbkey = this.model.get( 'dbkey' ),
visualizations = this.model.get( 'visualizations' ),
visualization_url = this.urls.visualization,
popup_menu_dict = {},
params = {
@@ -250,17 +267,10 @@ var HDAEditView = HDABaseView.extend( LoggableMixin ).extend(
// Add dbkey to params if it exists.
if( dbkey ){ params.dbkey = dbkey; }
if( !( this.model.hasData() )
|| !( visualizations && visualizations.length )
|| !( visualization_url ) ){
this.visualizationsButton = null;
return null;
}
// render the icon from template
this.visualizationsButton = new IconButtonView({ model : new IconButton({
title : _l( 'Visualize' ),
href : visualization_url,
href : this.urls.visualization,
icon_class : 'chart_curve'
})});
var $icon = this.visualizationsButton.render().$el;
@@ -299,6 +309,40 @@ var HDAEditView = HDABaseView.extend( LoggableMixin ).extend(
}
return $icon;
},
/** Render an icon-button or popupmenu of links based on the applicable visualizations
* @returns {jQuery} rendered DOM
*/
_render_visualizationsFrameworkButton : function( visualizations ){
if( !( this.model.hasData() )
|| !( visualizations && !_.isEmpty( visualizations ) ) ){
this.visualizationsButton = null;
return null;
}
// render the icon from template
this.visualizationsButton = new IconButtonView({ model : new IconButton({
title : _l( 'Visualize' ),
icon_class : 'chart_curve'
})});
var $icon = this.visualizationsButton.render().$el;
$icon.addClass( 'visualize-icon' ); // needed?
// No need for popup menu because there's a single visualization.
if( _.keys( visualizations ).length === 1 ) {
$icon.attr( 'title', _.keys( visualizations )[0] );
$icon.attr( 'href', _.values( visualizations )[0] );
// >1: Populate menu dict with visualization fns, make the popupmenu
} else {
var popup_menu_options = [];
_.each( visualizations, function( linkData ) {
popup_menu_options.push( linkData );
});
var popup = new PopupMenu( $icon, popup_menu_options );
}
return $icon;
},
// ......................................................................... secondary actions
/** Render secondary actions: currently tagging and annotation (if user is allowed).
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
define(["libs/underscore","viz/trackster/util"],function(b,e){var c=Backbone.Model.extend({initialize:function(f){var g=this.get("key");this.set("id",g);var h=b.find(c.known_settings_defaults,function(i){return i.key===g});if(h){this.set(b.extend({},h,f))}if(this.get("type")==="color"&&!this.get("value")){this.set("value",e.get_random_color())}this.on("change:value",this.cast_value,this)},cast_value:function(){var f=this.get("type"),g=this.get("value");if(f==="float"){g=parseFloat(g)}else{if(f==="int"){g=parseInt(g,10)}}this.set("value")}},{known_settings_defaults:[{key:"name",label:"Name",type:"text",default_value:""},{key:"color",label:"Color",type:"color",default_value:null},{key:"min_value",label:"Min Value",type:"float",default_value:null},{key:"max_value",label:"Max Value",type:"float",default_value:null},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"height",type:"int",default_value:32,hidden:true},{key:"pos_color",label:"Positive Color",type:"color",default_value:"#FF8C00"},{key:"neg_color",label:"Negative Color",type:"color",default_value:"#4169E1"},{key:"block_color",label:"Block color",type:"color",default_value:null},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_insertions",label:"Show insertions",type:"bool",default_value:false},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"reverse_strand_color",label:"Antisense strand color",type:"color",default_value:null},{key:"show_differences",label:"Show differences only",type:"bool",default_value:true},{key:"histogram_max",label:"Histogram maximum",type:"float",default_value:null,help:"Clear value to set automatically"},{key:"mode",type:"string",default_value:this.mode,hidden:true}]});var d=Backbone.Collection.extend({model:c,to_key_value_dict:function(){var f={};this.each(function(g){f[g.get("key")]=g.get("value")});return f},get_value:function(f){var g=this.get(f);if(g){return g.get("value")}return undefined}},{from_config_dict:function(g){var f=b.map(b.keys(g),function(h){return{key:h,value:g[h]}});return new d(f)}});var a=Backbone.View.extend({className:"config-settings-view",render:function(){var i=this.model;var f=this.$el;var h;function g(n,j){for(var r=0;r<n.length;r++){h=n[r];if(h.hidden){continue}var l="param_"+r;var v=i.values[h.key];var x=$("<div class='form-row' />").appendTo(j);x.append($("<label />").attr("for",l).text(h.label+":"));if(type==="bool"){x.append($('<input type="checkbox" />').attr("id",l).attr("name",l).attr("checked",v))}else{if(type==="text"){x.append($('<input type="text"/>').attr("id",l).val(v).click(function(){$(this).select()}))}else{if(type==="select"){var t=$("<select />").attr("id",l);for(var p=0;p<h.options.length;p++){$("<option/>").text(h.options[p].label).attr("value",h.options[p].value).appendTo(t)}t.val(v);x.append(t)}else{if(type==="color"){var w=$("<div/>").appendTo(x),s=$("<input />").attr("id",l).attr("name",l).val(v).css("float","left").appendTo(w).click(function(z){$(".bs-tooltip").removeClass("in");var y=$(this).siblings(".bs-tooltip").addClass("in");y.css({left:$(this).position().left+$(this).width()+5,top:$(this).position().top-($(y).height()/2)+($(this).height()/2)}).show();y.click(function(A){A.stopPropagation()});$(document).bind("click.color-picker",function(){y.hide();$(document).unbind("click.color-picker")});z.stopPropagation()}),q=$("<a href='javascript:void(0)'/>").addClass("icon-button arrow-circle").appendTo(w).attr("title","Set new random color").tooltip(),u=$("<div class='bs-tooltip right' style='position: absolute;' />").appendTo(w).hide(),m=$("<div class='tooltip-inner' style='text-align: inherit'></div>").appendTo(u),k=$("<div class='tooltip-arrow'></div>").appendTo(u),o=$.farbtastic(m,{width:100,height:100,callback:s,color:v});w.append($("<div/>").css("clear","both"));(function(y){q.click(function(){y.setColor(e.get_random_color())})})(o)}else{x.append($("<input />").attr("id",l).attr("name",l).val(v))}}}}if(h.help){x.append($("<div class='help'/>").text(h.help))}}}g(this.params,f);return this},render_in_modal:function(){var h=function(){hide_modal();$(window).unbind("keypress.check_enter_esc")},f=function(){this.update_from_form();hide_modal();$(window).unbind("keypress.check_enter_esc")},g=function(i){if((i.keyCode||i.which)===27){h()}else{if((i.keyCode||i.which)===13){f()}}};$(window).bind("keypress.check_enter_esc",g);if(this.$el.children().length===0){this.render()}show_modal("Configure",drawable.config.build_form(),{Cancel:h,OK:f})},update_from_form:function(){var f=this;this.collection.each(function(h,g){if(!h.get("hidden")){var j="param_"+g;var i=f.$el.find("#"+j).val();if(type==="bool"){i=container.find("#"+j).is(":checked")}h.set("value",i)}})}});return{ConfigSettingCollection:d,ConfigSettingCollectionView:a}});
define(["libs/underscore","viz/trackster/util"],function(b,e){var c=Backbone.Model.extend({initialize:function(f){var g=this.get("key");this.set("id",g);var h=b.find(c.known_settings_defaults,function(i){return i.key===g});if(h){this.set(b.extend({},h,f))}if(this.get("type")==="color"&&!this.get("value")){this.set("value",e.get_random_color())}this.on("change:value",this.cast_value,this)},cast_value:function(){var f=this.get("type"),g=this.get("value");if(f==="float"){g=parseFloat(g)}else{if(f==="int"){g=parseInt(g,10)}}this.set("value")}},{known_settings_defaults:[{key:"name",label:"Name",type:"text",default_value:""},{key:"color",label:"Color",type:"color",default_value:null},{key:"min_value",label:"Min Value",type:"float",default_value:null},{key:"max_value",label:"Max Value",type:"float",default_value:null},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"height",type:"int",default_value:32,hidden:true},{key:"pos_color",label:"Positive Color",type:"color",default_value:"#FF8C00"},{key:"neg_color",label:"Negative Color",type:"color",default_value:"#4169E1"},{key:"block_color",label:"Block color",type:"color",default_value:null},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_insertions",label:"Show insertions",type:"bool",default_value:false},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"reverse_strand_color",label:"Antisense strand color",type:"color",default_value:null},{key:"show_differences",label:"Show differences only",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true}]});var d=Backbone.Collection.extend({model:c,to_key_value_dict:function(){var f={};this.each(function(g){f[g.get("key")]=g.get("value")});return f},get_value:function(f){var g=this.get(f);if(g){return g.get("value")}return undefined}},{from_config_dict:function(g){var f=b.map(b.keys(g),function(h){return{key:h,value:g[h]}});return new d(f)}});var a=Backbone.View.extend({className:"config-settings-view",render:function(){var i=this.model;var f=this.$el;var h;function g(n,j){for(var r=0;r<n.length;r++){h=n[r];if(h.hidden){continue}var l="param_"+r;var v=i.values[h.key];var x=$("<div class='form-row' />").appendTo(j);x.append($("<label />").attr("for",l).text(h.label+":"));if(type==="bool"){x.append($('<input type="checkbox" />').attr("id",l).attr("name",l).attr("checked",v))}else{if(type==="text"){x.append($('<input type="text"/>').attr("id",l).val(v).click(function(){$(this).select()}))}else{if(type==="select"){var t=$("<select />").attr("id",l);for(var p=0;p<h.options.length;p++){$("<option/>").text(h.options[p].label).attr("value",h.options[p].value).appendTo(t)}t.val(v);x.append(t)}else{if(type==="color"){var w=$("<div/>").appendTo(x),s=$("<input />").attr("id",l).attr("name",l).val(v).css("float","left").appendTo(w).click(function(z){$(".bs-tooltip").removeClass("in");var y=$(this).siblings(".bs-tooltip").addClass("in");y.css({left:$(this).position().left+$(this).width()+5,top:$(this).position().top-($(y).height()/2)+($(this).height()/2)}).show();y.click(function(A){A.stopPropagation()});$(document).bind("click.color-picker",function(){y.hide();$(document).unbind("click.color-picker")});z.stopPropagation()}),q=$("<a href='javascript:void(0)'/>").addClass("icon-button arrow-circle").appendTo(w).attr("title","Set new random color").tooltip(),u=$("<div class='bs-tooltip right' style='position: absolute;' />").appendTo(w).hide(),m=$("<div class='tooltip-inner' style='text-align: inherit'></div>").appendTo(u),k=$("<div class='tooltip-arrow'></div>").appendTo(u),o=$.farbtastic(m,{width:100,height:100,callback:s,color:v});w.append($("<div/>").css("clear","both"));(function(y){q.click(function(){y.setColor(e.get_random_color())})})(o)}else{x.append($("<input />").attr("id",l).attr("name",l).val(v))}}}}if(h.help){x.append($("<div class='help'/>").text(h.help))}}}g(this.params,f);return this},render_in_modal:function(){var h=function(){hide_modal();$(window).unbind("keypress.check_enter_esc")},f=function(){this.update_from_form();hide_modal();$(window).unbind("keypress.check_enter_esc")},g=function(i){if((i.keyCode||i.which)===27){h()}else{if((i.keyCode||i.which)===13){f()}}};$(window).bind("keypress.check_enter_esc",g);if(this.$el.children().length===0){this.render()}show_modal("Configure",drawable.config.build_form(),{Cancel:h,OK:f})},update_from_form:function(){var f=this;this.collection.each(function(h,g){if(!h.get("hidden")){var j="param_"+g;var i=f.$el.find("#"+j).val();if(type==="bool"){i=container.find("#"+j).is(":checked")}h.set("value",i)}})}});return{ConfigSettingCollection:d,ConfigSettingCollectionView:a}});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -222,9 +222,9 @@ ${h.js(
<script type="text/javascript">
$(function(){
var hda = ${h.to_json_string( hda )},
historyID = '${historyID}',
querySettings = ${h.to_json_string( kwargs )},
var hda = ${h.to_json_string( hda.get_api_value() )},
historyID = '${trans.security.encode_id( hda.history.id )}',
querySettings = ${h.to_json_string( query_args )},
chartConfig = _.extend( querySettings, {
containerSelector : '#chart',
//TODO: move to ScatterplotControlForm.initialize
@@ -250,8 +250,8 @@ $(function(){
<%def name="body()">
<!--dataset info-->
<div id="chart-header" class="header">
<h2 class="title">Scatterplot of '${hda['name']}'</h2>
<p class="subtitle">${hda['misc_info']}</p>
<h2 class="title">Scatterplot of '${hda.name}'</h2>
<p class="subtitle">${hda.info}</p>
</div>
<div id="scatterplot" class="scatterplot-control-form"></div>
</%def>
@@ -0,0 +1,132 @@
<%inherit file="/base.mako"/>
<%def name="title()">
${visualization_name}
</%def>
<%def name="stylesheets()">
${parent.stylesheets()}
${h.css(
"base",
)}
<style type="text/css">
/*TODO: use/move into base.less*/
* { margin: 0px; padding: 0px; }
</style>
</%def>
<%def name="process_hda( hda )">
<%
hda_dict = hda.get_api_value()
hda_dict[ 'id' ] = trans.security.encode_id( hda_dict[ 'id' ] )
hda_dict[ 'history_id' ] = trans.security.encode_id( hda_dict[ 'history_id' ] )
del hda_dict[ 'peek' ]
return hda_dict
%>
</%def>
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
//var data = {
// title : 'shared with user visualization',
// type : 'test',
// slug : 'shared',
// annotation : 'a visualization shared with a specific user',
// config : {
// x : 10,
// y : 10
// }
// };
//var creationPromise = jQuery.ajax( '/api/visualizations', {
// type : 'POST',
// contentType : 'application/json',
// data : JSON.stringify( data )
// });
//creationPromise.success(function(){
// console.debug( 'success' );
//});
//creationPromise.error(function(){
// console.debug( 'error' );
//});
});
</script>
</%def>
<%def name="print_var( name, var )">
%if var is not None:
<% t = str( type( var ) )[1:-1] %>
<p>${name}: ${t}, ${var}</p>
%else:
<p>No ${name}</p>
%endif
</%def>
<%def name="body()">
<%
import pprint
print self
print self.context
pprint.pprint( self.context.kwargs, indent=4 )
%>
<%
vars_to_print = [
( 'default', default ),
( 'string', string ),
( 'boolean', boolean ),
( 'integer', integer ),
( 'float', float ),
( 'json', json ),
]
%>
%for name, var in vars_to_print:
${print_var( name, var )}
%endfor
%if visualization:
<h1>${visualization.title}</h1>
<p>id: ${trans.security.encode_id( visualization.id )}</p>
<p>dbkey: ${visualization.dbkey}</p>
<p>config:
<pre>${h.to_json_string( visualization.latest_revision.config, sort_keys=True, indent=( 4 * ' ' ) )}</pre>
</p>
%endif
%if dataset:
<h1>${dataset.name}</h1>
<p>id: ${trans.security.encode_id( dataset.id )}</p>
<p>history id: ${trans.security.encode_id( dataset.history.id )}</p>
<pre>
${h.to_json_string( process_hda( dataset ), sort_keys=True, indent=( 4 * ' ' ) )}
</pre>
%endif
%if dataset_instance:
<h1>${dataset_instance.name}</h1>
<p>id: ${trans.security.encode_id( dataset_instance.id )}</p>
%if hda_ldda == 'hda':
<p>history id: ${trans.security.encode_id( dataset_instance.history.id )}</p>
<pre>
${h.to_json_string( process_hda( dataset_instance ), sort_keys=True, indent=( 4 * ' ' ) )}
</pre>
%else:
<p>(LibraryDatasetDatasetAssociation)</p>
%endif
%endif
%if query_args:
<ul>
%for key, val in query_args.items():
<li>${key} : ${val}</li>
%endfor
</ul>
%endif
</%def>
+4
View File
@@ -171,6 +171,10 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# Galaxy.
#datatypes_config_file = datatypes_conf.xml
# Visualizations config file, defines what visualizations apply to particular data and how to pass them
# the necessary parameters
#visualizations_conf_path = visualizations_conf.xml
# Each job is given a unique empty directory as its current working directory.
# This option defines in what parent directory those directories will be
# created.
+292
View File
@@ -0,0 +1,292 @@
<?xml version="1.0"?>
<!--
This is the xml file to edit to add new visualizations to the framework.
NOTE!: this is a work in progress!
Note: also that visualizations that fail to parse in visualizations/registry will
produce an error in the server log, but otherwise will be skipped and not available.
If you can't find your visualization in the UI, check the server logs for errors
during start up.
-->
<!-- .......................................................................... DTD -->
<!-- runnable on NIX with xmllint -->
<!DOCTYPE visualizations [
<!-- 0 or more visualizations -->
<!ELEMENT visualizations (visualization*)>
<!-- each visualization must have a template (all other elements are optional) -->
<!ELEMENT visualization (data_sources*,params*,template_root*,template,link_text*,render_location*)>
<!-- visualization name (e.g. 'trackster', 'scatterplot', etc.) is required -->
<!ATTLIST visualization
name CDATA #REQUIRED
>
<!ELEMENT data_sources (data_source*)>
<!-- data sources are elements that describe what objects (HDAs, LDDAs, Job, User, etc.)
are applicable to a visualization. Often these are used to fetch applicable links
to the visualizations that use them.
-->
<!ELEMENT data_source (model_class,(test|to_param)*)>
<!ELEMENT model_class (#PCDATA)>
<!-- model_class is currently the class name of the object you want to make a visualization
applicable to (e.g. HistoryDatasetAssociation). Currently only classes in galaxy.model
can be used.
REQUIRED and currently limited to: 'HistoryDatasetAssociation', 'LibraryDatasetDatasetAssociation'
-->
<!ELEMENT test (#PCDATA)>
<!-- tests help define what conditions the visualization can be applied to the model_class/target.
Currently, all tests are OR'd and there is no logical grouping. Tests are run in order.
(text): the text of this element is what the given target will be compared to (REQUIRED)
type: what type of test to run (e.g. when the target is an HDA the test will often be of type 'isinstance'
and test whether the HDA's datatype isinstace of a class)
DEFAULT: string comparison.
test_attr: what attribute of the target object should be used in the test. For instance, 'datatype'
will attempt to get the HDA.datatype from a target HDA. If the given object doesn't have
that attribute the test will fail (with no error). test_attr can be dot separated attributes,
looking up each in turn. For example, if the target was a history, one could access the
history.user.email by setting test_attr to 'user.email' (why you would want that, I don't know)
DEFAULT: to comparing the object itself (and not any of it's attributes)
result_type: if the result (the text of the element mentioned above) needs to be parsed into
something other than a string, result_type will tell the registry how to do this. E.g.
if result_type is 'datatype' the registry will assume the text is a datatype class name
and parse it into the proper class before the test (often 'isinstance') is run.
DEFAULT: no parsing (result should be a string)
-->
<!ATTLIST test
type CDATA #IMPLIED
test_attr CDATA #IMPLIED
result_type CDATA #IMPLIED
>
<!ELEMENT to_param (#PCDATA)>
<!-- to_param tells the registry how to parse the data_source into a query string param.
For example, HDA data_sources can set param_to text to 'dataset_id' and param_attr to 'id' and the
the target HDA (if it passes the tests) will be passed as "dataset_id=HDA.id"
(text): the query string param key this source will be parsed into (e.g. dataset_id)
REQUIRED
param_attr: the attribute of the data_source object to use as the value in the query string param.
E.g. param_attr='id' for an HDA data_source would use the (encoded) id.
NOTE: a to_param MUST have either a param_attr or assign
assign: you can use this to directly assign a value to a query string's param. E.g. if the
data_source is a LDDA we can set 'hda_or_ldda=ldda' using assign='ldda'.
NOTE: a to_param MUST have either a param_attr or assign
-->
<!ATTLIST to_param
param_attr CDATA #IMPLIED
assign CDATA #IMPLIED
>
<!ELEMENT params ((param|param_modifier)*)>
<!-- params describe what data will be sent to a visualization template and
how to convert them from a query string in a URL into variables usable in a template.
For example,
param_modifiers are a special class of parameters that modify other params
(e.g. hda_ldda can be 'hda' or 'ldda' and modifies/informs dataset_id to fetch an HDA or LDDA)
-->
<!ELEMENT param (#PCDATA)>
<!-- param tells the registry how to parse the query string param back into a resource/data_source.
For example, if a query string has "dataset_id=NNN" and the type is 'dataset', the registry
will attempt to fetch the hda with id of NNN from the database and pass it to the template.
(text): the query string param key this source will be parsed from (e.g. dataset_id)
REQUIRED
type: the type of the resource.
Can be: str (DEFAULT), bool, int, float, json, visualization, dbkey, dataset, or hda_ldda.
default: if a param is not passed on the query string (and is not required) OR the given param
fails to parse, this value is used instead.
DEFAULT: None
required: set this to true if the param is required for the template. Rendering will with an error
if the param hasn't been sent.
DEFAULT: false
csv: set this to true if the param is a comma separated list. The registry will attempt to
parse each value as the given type and send the result as a list to the template.
DEFAULT: false
constrain_to: (currently unused) constain a param to a set of values, error if not valid.
DEFAULT: don't constrain
var_name_in_template: a new name for the resource/variable to use in the template. E.g. an initial
query string param key might be 'dataset_id' in the URL, the registry parses it into an HDA,
and if var_name_in_template is set to 'hda', the template will be able to access the HDA
with the variable name 'hda' (as in hda.title).
DEFAULT: keep the original query string name
-->
<!ATTLIST param
type CDATA #IMPLIED
default CDATA #IMPLIED
required CDATA #IMPLIED
csv CDATA #IMPLIED
constrain_to CDATA #IMPLIED
var_name_in_template CDATA #IMPLIED
>
<!-- param_modifiers are the same as param but have a REQUIRED 'modifies' attribute.
'modifies' must point to the param name (the text part of param element) that it will modify.
E.g. <param_modifier modifies="dataset_id">hda_ldda</param_modifier>
-->
<!ELEMENT param_modifier (#PCDATA)>
<!ATTLIST param_modifier
modifies CDATA #REQUIRED
type CDATA #IMPLIED
default CDATA #IMPLIED
required CDATA #IMPLIED
csv CDATA #IMPLIED
constrain_to CDATA #IMPLIED
var_name_in_template CDATA #IMPLIED
>
<!-- template_root: the directory to search for the template relative to templates/webapps/galaxy
(optional) DEFAULT: visualizations
-->
<!ELEMENT template_root (#PCDATA)>
<!-- template: the template used to render the visualization. REQUIRED -->
<!ELEMENT template (#PCDATA)>
<!-- link_text: the text component of an html anchor displayed when the registry builds the link information -->
<!ELEMENT link_text (#PCDATA)>
<!-- render_location: used as the target attribute of the link to the visualization.
Can be 'galaxy_main', '_top', '_blank'. DEFAULT: 'galaxy_main'
-->
<!-- TODO: rename -> render_target -->
<!ELEMENT render_location (#PCDATA)>
]>
<!-- .......................................................................... configuration xml -->
<visualizations>
<visualization name="trackster">
<!--not tested yet -->
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="hda">hda_ldda</to_param>
<to_param param_attr="dbkey">dbkey</to_param>
</data_source>
<data_source>
<model_class>LibraryDatasetDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="ldda">hda_ldda</to_param>
</data_source>
</data_sources>
<params>
<param type="visualization">id</param>
<param type="dataset">dataset_id</param>
<param type="genome_region">genome_region</param>
<param type="dbkey">dbkey</param>
</params>
<template_root>tracks</template_root>
<template>browser.mako</template>
<render_location>_top</render_location>
</visualization>
<visualization name="circster">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="hda">hda_ldda</to_param>
</data_source>
<data_source>
<model_class>LibraryDatasetDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="ldda">hda_ldda</to_param>
</data_source>
</data_sources>
<params>
<param type="visualization">id</param>
<param type="hda_or_ldda">dataset_id</param>
<param_modifier type="string" modifies="dataset_id">hda_ldda</param_modifier>
<param type="dbkey">dbkey</param>
</params>
<template>circster.mako</template>
<render_location>_top</render_location>
</visualization>
<!--
<visualization name="sweepster">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="hda">hda_ldda</to_param>
</data_source>
<data_source>
<model_class>LibraryDatasetDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="ldda">hda_ldda</to_param>
</data_source>
</data_sources>
<params>
<param type="visualization" var_name_in_template="viz">visualization</param>
<param type="hda_or_ldda" var_name_in_template="dataset">dataset_id</param>
<param_modifier type="string" modifies="dataset_id">hda_ldda</param_modifier>
</params>
<template>sweepster.mako</template>
<render_location>_top</render_location>
</visualization>
-->
<visualization name="phyloviz">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Newick</test>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Nexus</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
<param type="integer" default="0">tree_index</param>
</params>
<template>phyloviz.mako</template>
<render_location>_top</render_location>
</visualization>
<visualization name="scatterplot">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<template>scatterplot.mako</template>
</visualization>
<!--
<visualization name="test">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Data</test>
<to_param param_attr="id">dataset_id</to_param>
<to_param assign="bler">string</to_param>
<to_param assign="False">boolean</to_param>
<to_param assign="-5">integer</to_param>
<to_param assign="3.14">float</to_param>
<to_param assign="{}">json</to_param>
</data_source>
</data_sources>
<params>
<param>default</param>
<param type="str">string</param>
<param type="bool">boolean</param>
<param type="int">integer</param>
<param type="float">float</param>
<param type="json">json</param>
<param type="str" required="true">string</param>
<param type="visualization" var_name_in_template="visualization">visualization_id</param>
<param type="dataset" var_name_in_template="dataset">dataset_id</param>
<param type="hda_or_ldda">dataset_instance</param>
<param_modifier type="str" modifies="dataset_instance">hda_ldda</param_modifier>
</params>
<template>v_fwork_test.mako</template>
</visualization>
-->
</visualizations>