From df1927422c8e1084fed09bfb4b94f8614bf4b2e1 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 17 May 2013 14:45:07 -0400 Subject: [PATCH] Visualizations framework: allow users to associate custom visualizations with models, datatypes, etc. via an xml configuration file (visualizations_conf.xml) --- .hgignore | 2 +- lib/galaxy/app.py | 4 + lib/galaxy/config.py | 2 + lib/galaxy/datatypes/registry.py | 30 + .../visualization/data_providers/registry.py | 6 +- lib/galaxy/visualization/registry.py | 808 ++++++++++++++++++ lib/galaxy/web/base/controller.py | 249 +++++- lib/galaxy/webapps/galaxy/buildapp.py | 11 +- .../galaxy/controllers/visualization.py | 58 ++ static/scripts/mvc/dataset/hda-edit.js | 62 +- static/scripts/packed/mvc/dataset/hda-edit.js | 2 +- static/scripts/packed/utils/config.js | 2 +- .../scripts/packed/viz/trackster/painters.js | 2 +- static/scripts/packed/viz/trackster/tracks.js | 2 +- static/scripts/packed/viz/visualization.js | 2 +- .../galaxy/visualization/scatterplot.mako | 10 +- .../galaxy/visualization/v_fwork_test.mako | 132 +++ universe_wsgi.ini.sample | 4 + visualizations_conf.xml.sample | 292 +++++++ 19 files changed, 1630 insertions(+), 50 deletions(-) create mode 100644 lib/galaxy/visualization/registry.py create mode 100644 templates/webapps/galaxy/visualization/v_fwork_test.mako create mode 100644 visualizations_conf.xml.sample diff --git a/.hgignore b/.hgignore index f3be6b4df7f..b5acfbd1abf 100644 --- a/.hgignore +++ b/.hgignore @@ -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 diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 4c37bde4fda..a9ac0392133 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -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 ) diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 3e02987b0f2..297eee9ba84 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -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 ): diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index b268a214fce..3f2fab7f60a 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -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' ): diff --git a/lib/galaxy/visualization/data_providers/registry.py b/lib/galaxy/visualization/data_providers/registry.py index a86cd73a2cd..185ee3ecf03 100644 --- a/lib/galaxy/visualization/data_providers/registry.py +++ b/lib/galaxy/visualization/data_providers/registry.py @@ -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 \ No newline at end of file + return data_provider diff --git a/lib/galaxy/visualization/registry.py b/lib/galaxy/visualization/registry.py new file mode 100644 index 00000000000..4ca379801e2 --- /dev/null +++ b/lib/galaxy/visualization/registry.py @@ -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 + # + + 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=). + """ + #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 diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 08f1e6677b6..c00b0524aa3 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -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: diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index d1cf3b2e004..5123182dcd3 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -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"])) diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index 365714f389e..82d98f92758 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -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." + + "
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, diff --git a/static/scripts/mvc/dataset/hda-edit.js b/static/scripts/mvc/dataset/hda-edit.js index cdf8eb3b6cc..52476fdaaaa 100644 --- a/static/scripts/mvc/dataset/hda-edit.js +++ b/static/scripts/mvc/dataset/hda-edit.js @@ -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). diff --git a/static/scripts/packed/mvc/dataset/hda-edit.js b/static/scripts/packed/mvc/dataset/hda-edit.js index c5d293893d7..2c9b7bc2acd 100644 --- a/static/scripts/packed/mvc/dataset/hda-edit.js +++ b/static/scripts/packed/mvc/dataset/hda-edit.js @@ -1 +1 @@ -var HDAEditView=HDABaseView.extend(LoggableMixin).extend({initialize:function(a){HDABaseView.prototype.initialize.call(this,a);this.defaultPrimaryActionButtonRenderers=[this._render_showParamsButton,this._render_rerunButton]},_setUpBehaviors:function(c){HDABaseView.prototype._setUpBehaviors.call(this,c);var a=this,b=this.urls.purge,d=c.find("#historyItemPurger-"+this.model.get("id"));if(d){d.attr("href",["javascript","void(0)"].join(":"));d.click(function(e){var f=jQuery.ajax(b);f.success(function(i,g,h){a.model.set("purged",true);a.trigger("purged",a)});f.error(function(h,g,i){a.trigger("error",_l("Unable to purge this dataset"),h,g,i)})})}},_render_warnings:function(){return $(jQuery.trim(HDABaseView.templates.messages(_.extend(this.model.toJSON(),{urls:this.urls}))))},_render_titleButtons:function(){var a=$('
');a.append(this._render_displayButton());a.append(this._render_editButton());a.append(this._render_deleteButton());return a},_render_editButton:function(){if((this.model.get("state")===HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===HistoryDatasetAssociation.STATES.UPLOAD)||(this.model.get("state")===HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){this.editButton=null;return null}var c=this.model.get("purged"),a=this.model.get("deleted"),b={title:_l("Edit Attributes"),href:this.urls.edit,target:"galaxy_main",icon_class:"edit"};if(a||c){b.enabled=false;if(c){b.title=_l("Cannot edit attributes of datasets removed from disk")}else{if(a){b.title=_l("Undelete dataset to edit attributes")}}}this.editButton=new IconButtonView({model:new IconButton(b)});return this.editButton.render().$el},_render_deleteButton:function(){if((this.model.get("state")===HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){this.deleteButton=null;return null}var a=this,b=a.urls["delete"],c={title:_l("Delete"),href:b,id:"historyItemDeleter-"+this.model.get("id"),icon_class:"delete",on_click:function(){$.ajax({url:b,type:"POST",error:function(){a.$el.show()},success:function(){a.model.set({deleted:true})}})}};if(this.model.get("deleted")||this.model.get("purged")){c={title:_l("Dataset is already deleted"),icon_class:"delete",enabled:false}}this.deleteButton=new IconButtonView({model:new IconButton(c)});return this.deleteButton.render().$el},_render_hdaSummary:function(){var a=_.extend(this.model.toJSON(),{urls:this.urls});if(this.model.get("metadata_dbkey")==="?"&&!this.model.isDeletedOrPurged()){_.extend(a,{dbkey_unknown_and_editable:true})}return HDABaseView.templates.hdaSummary(a)},_render_errButton:function(){if(this.model.get("state")!==HistoryDatasetAssociation.STATES.ERROR){this.errButton=null;return null}this.errButton=new IconButtonView({model:new IconButton({title:_l("View or report this error"),href:this.urls.report_error,target:"galaxy_main",icon_class:"bug"})});return this.errButton.render().$el},_render_rerunButton:function(){this.rerunButton=new IconButtonView({model:new IconButton({title:_l("Run this job again"),href:this.urls.rerun,target:"galaxy_main",icon_class:"arrow-circle"})});return this.rerunButton.render().$el},_render_visualizationsButton:function(){var c=this.model.get("dbkey"),a=this.model.get("visualizations"),f=this.urls.visualization,d={},g={dataset_id:this.model.get("id"),hda_ldda:"hda"};if(c){g.dbkey=c}if(!(this.model.hasData())||!(a&&a.length)||!(f)){this.visualizationsButton=null;return null}this.visualizationsButton=new IconButtonView({model:new IconButton({title:_l("Visualize"),href:f,icon_class:"chart_curve"})});var b=this.visualizationsButton.render().$el;b.addClass("visualize-icon");function e(h){switch(h){case"trackster":return create_trackster_action_fn(f,g,c);case"scatterplot":return create_scatterplot_action_fn(f,g);default:return function(){window.parent.location=f+"/"+h+"?"+$.param(g)}}}if(a.length===1){b.attr("title",a[0]);b.click(e(a[0]))}else{_.each(a,function(i){var h=i.charAt(0).toUpperCase()+i.slice(1);d[_l(h)]=e(i)});make_popupmenu(b,d)}return b},_render_secondaryActionButtons:function(b){var c=$("
"),a=this;c.attr("style","float: right;").attr("id","secondary-actions-"+this.model.get("id"));_.each(b,function(d){c.append(d.call(a))});return c},_render_tagButton:function(){if(!(this.model.hasData())||(!this.urls.tags.get)){this.tagButton=null;return null}this.tagButton=new IconButtonView({model:new IconButton({title:_l("Edit dataset tags"),target:"galaxy_main",href:this.urls.tags.get,icon_class:"tags"})});return this.tagButton.render().$el},_render_annotateButton:function(){if(!(this.model.hasData())||(!this.urls.annotation.get)){this.annotateButton=null;return null}this.annotateButton=new IconButtonView({model:new IconButton({title:_l("Edit dataset annotation"),target:"galaxy_main",icon_class:"annotate"})});return this.annotateButton.render().$el},_render_tagArea:function(){if(!this.urls.tags.set){return null}return $(HDAEditView.templates.tagArea(_.extend(this.model.toJSON(),{urls:this.urls})))},_render_annotationArea:function(){if(!this.urls.annotation.get){return null}return $(HDAEditView.templates.annotationArea(_.extend(this.model.toJSON(),{urls:this.urls})))},_render_body_error:function(a){HDABaseView.prototype._render_body_error.call(this,a);var b=a.find("#primary-actions-"+this.model.get("id"));b.prepend(this._render_errButton())},_render_body_ok:function(a){a.append(this._render_hdaSummary());if(this.model.isDeletedOrPurged()){a.append(this._render_primaryActionButtons([this._render_downloadButton,this._render_showParamsButton,this._render_rerunButton]));return}a.append(this._render_primaryActionButtons([this._render_downloadButton,this._render_showParamsButton,this._render_rerunButton,this._render_visualizationsButton]));a.append(this._render_secondaryActionButtons([this._render_tagButton,this._render_annotateButton]));a.append('
');a.append(this._render_tagArea());a.append(this._render_annotationArea());a.append(this._render_displayAppArea());this._render_displayApps(a);a.append(this._render_peek())},events:{"click .historyItemTitle":"toggleBodyVisibility","click a.icon-button.tags":"loadAndDisplayTags","click a.icon-button.annotate":"loadAndDisplayAnnotation"},loadAndDisplayTags:function(c){this.log(this+".loadAndDisplayTags",c);var a=this,d=this.$el.find(".tag-area"),b=d.find(".tag-elt");if(d.is(":hidden")){if(!jQuery.trim(b.html())){$.ajax({url:this.urls.tags.get,error:function(g,e,f){a.log("Tagging failed",g,e,f);a.trigger("error",_l("Tagging failed"),g,e,f)},success:function(e){b.html(e);b.find(".tooltip").tooltip();d.slideDown("fast")}})}else{d.slideDown("fast")}}else{d.slideUp("fast")}return false},loadAndDisplayAnnotation:function(b){this.log(this+".loadAndDisplayAnnotation",b);var d=this.$el.find(".annotation-area"),c=d.find(".annotation-elt"),a=this.urls.annotation.set;if(d.is(":hidden")){if(!jQuery.trim(c.html())){$.ajax({url:this.urls.annotation.get,error:function(){view.log("Annotation failed",xhr,status,error);view.trigger("error",_l("Annotation failed"),xhr,status,error)},success:function(e){if(e===""){e=""+_l("Describe or add notes to dataset")+""}c.html(e);d.find(".tooltip").tooltip();async_save_text(c.attr("id"),c.attr("id"),a,"new_annotation",18,true,4);d.slideDown("fast")}})}else{d.slideDown("fast")}}else{d.slideUp("fast")}return false},toString:function(){var a=(this.model)?(this.model+""):("(no model)");return"HDAView("+a+")"}});HDAEditView.templates={tagArea:Handlebars.templates["template-hda-tagArea"],annotationArea:Handlebars.templates["template-hda-annotationArea"]};function create_scatterplot_action_fn(a,b){action=function(){var d=$(window.parent.document).find("iframe#galaxy_main"),c=a+"/scatterplot?"+$.param(b);d.attr("src",c);$("div.popmenu-wrapper").remove();return false};return action}function create_trackster_action_fn(a,c,b){return function(){var d={};if(b){d["f-dbkey"]=b}$.ajax({url:a+"/list_tracks?"+$.param(d),dataType:"html",error:function(){alert(_l("Could not add this dataset to browser")+".")},success:function(e){var f=window.parent;f.show_modal(_l("View Data in a New or Saved Visualization"),"",{Cancel:function(){f.hide_modal()},"View in saved visualization":function(){f.show_modal(_l("Add Data to Saved Visualization"),e,{Cancel:function(){f.hide_modal()},"Add to visualization":function(){$(f.document).find("input[name=id]:checked").each(function(){var g=$(this).val();c.id=g;f.location=a+"/trackster?"+$.param(c)})}})},"View in new visualization":function(){f.location=a+"/trackster?"+$.param(c)}})}});return false}}; \ No newline at end of file +var HDAEditView=HDABaseView.extend(LoggableMixin).extend({initialize:function(a){HDABaseView.prototype.initialize.call(this,a);this.defaultPrimaryActionButtonRenderers=[this._render_showParamsButton,this._render_rerunButton]},_setUpBehaviors:function(c){HDABaseView.prototype._setUpBehaviors.call(this,c);var a=this,b=this.urls.purge,d=c.find("#historyItemPurger-"+this.model.get("id"));if(d){d.attr("href",["javascript","void(0)"].join(":"));d.click(function(e){var f=jQuery.ajax(b);f.success(function(i,g,h){a.model.set("purged",true);a.trigger("purged",a)});f.error(function(h,g,i){a.trigger("error",_l("Unable to purge this dataset"),h,g,i)})})}},_render_warnings:function(){return $(jQuery.trim(HDABaseView.templates.messages(_.extend(this.model.toJSON(),{urls:this.urls}))))},_render_titleButtons:function(){var a=$('
');a.append(this._render_displayButton());a.append(this._render_editButton());a.append(this._render_deleteButton());return a},_render_editButton:function(){if((this.model.get("state")===HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===HistoryDatasetAssociation.STATES.UPLOAD)||(this.model.get("state")===HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){this.editButton=null;return null}var c=this.model.get("purged"),a=this.model.get("deleted"),b={title:_l("Edit Attributes"),href:this.urls.edit,target:"galaxy_main",icon_class:"edit"};if(a||c){b.enabled=false;if(c){b.title=_l("Cannot edit attributes of datasets removed from disk")}else{if(a){b.title=_l("Undelete dataset to edit attributes")}}}this.editButton=new IconButtonView({model:new IconButton(b)});return this.editButton.render().$el},_render_deleteButton:function(){if((this.model.get("state")===HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){this.deleteButton=null;return null}var a=this,b=a.urls["delete"],c={title:_l("Delete"),href:b,id:"historyItemDeleter-"+this.model.get("id"),icon_class:"delete",on_click:function(){$.ajax({url:b,type:"POST",error:function(){a.$el.show()},success:function(){a.model.set({deleted:true})}})}};if(this.model.get("deleted")||this.model.get("purged")){c={title:_l("Dataset is already deleted"),icon_class:"delete",enabled:false}}this.deleteButton=new IconButtonView({model:new IconButton(c)});return this.deleteButton.render().$el},_render_hdaSummary:function(){var a=_.extend(this.model.toJSON(),{urls:this.urls});if(this.model.get("metadata_dbkey")==="?"&&!this.model.isDeletedOrPurged()){_.extend(a,{dbkey_unknown_and_editable:true})}return HDABaseView.templates.hdaSummary(a)},_render_errButton:function(){if(this.model.get("state")!==HistoryDatasetAssociation.STATES.ERROR){this.errButton=null;return null}this.errButton=new IconButtonView({model:new IconButton({title:_l("View or report this error"),href:this.urls.report_error,target:"galaxy_main",icon_class:"bug"})});return this.errButton.render().$el},_render_rerunButton:function(){this.rerunButton=new IconButtonView({model:new IconButton({title:_l("Run this job again"),href:this.urls.rerun,target:"galaxy_main",icon_class:"arrow-circle"})});return this.rerunButton.render().$el},_render_visualizationsButton:function(){var a=this.model.get("visualizations");if((!this.model.hasData())||(_.isEmpty(a))){this.visualizationsButton=null;return null}if(_.isObject(a[0])){return this._render_visualizationsFrameworkButton(a)}if(!this.urls.visualization){this.visualizationsButton=null;return null}var c=this.model.get("dbkey"),f=this.urls.visualization,d={},g={dataset_id:this.model.get("id"),hda_ldda:"hda"};if(c){g.dbkey=c}this.visualizationsButton=new IconButtonView({model:new IconButton({title:_l("Visualize"),href:this.urls.visualization,icon_class:"chart_curve"})});var b=this.visualizationsButton.render().$el;b.addClass("visualize-icon");function e(h){switch(h){case"trackster":return create_trackster_action_fn(f,g,c);case"scatterplot":return create_scatterplot_action_fn(f,g);default:return function(){window.parent.location=f+"/"+h+"?"+$.param(g)}}}if(a.length===1){b.attr("title",a[0]);b.click(e(a[0]))}else{_.each(a,function(i){var h=i.charAt(0).toUpperCase()+i.slice(1);d[_l(h)]=e(i)});make_popupmenu(b,d)}return b},_render_visualizationsFrameworkButton:function(a){if(!(this.model.hasData())||!(a&&!_.isEmpty(a))){this.visualizationsButton=null;return null}this.visualizationsButton=new IconButtonView({model:new IconButton({title:_l("Visualize"),icon_class:"chart_curve"})});var c=this.visualizationsButton.render().$el;c.addClass("visualize-icon");if(_.keys(a).length===1){c.attr("title",_.keys(a)[0]);c.attr("href",_.values(a)[0])}else{var d=[];_.each(a,function(e){d.push(e)});var b=new PopupMenu(c,d)}return c},_render_secondaryActionButtons:function(b){var c=$("
"),a=this;c.attr("style","float: right;").attr("id","secondary-actions-"+this.model.get("id"));_.each(b,function(d){c.append(d.call(a))});return c},_render_tagButton:function(){if(!(this.model.hasData())||(!this.urls.tags.get)){this.tagButton=null;return null}this.tagButton=new IconButtonView({model:new IconButton({title:_l("Edit dataset tags"),target:"galaxy_main",href:this.urls.tags.get,icon_class:"tags"})});return this.tagButton.render().$el},_render_annotateButton:function(){if(!(this.model.hasData())||(!this.urls.annotation.get)){this.annotateButton=null;return null}this.annotateButton=new IconButtonView({model:new IconButton({title:_l("Edit dataset annotation"),target:"galaxy_main",icon_class:"annotate"})});return this.annotateButton.render().$el},_render_tagArea:function(){if(!this.urls.tags.set){return null}return $(HDAEditView.templates.tagArea(_.extend(this.model.toJSON(),{urls:this.urls})))},_render_annotationArea:function(){if(!this.urls.annotation.get){return null}return $(HDAEditView.templates.annotationArea(_.extend(this.model.toJSON(),{urls:this.urls})))},_render_body_error:function(a){HDABaseView.prototype._render_body_error.call(this,a);var b=a.find("#primary-actions-"+this.model.get("id"));b.prepend(this._render_errButton())},_render_body_ok:function(a){a.append(this._render_hdaSummary());if(this.model.isDeletedOrPurged()){a.append(this._render_primaryActionButtons([this._render_downloadButton,this._render_showParamsButton,this._render_rerunButton]));return}a.append(this._render_primaryActionButtons([this._render_downloadButton,this._render_showParamsButton,this._render_rerunButton,this._render_visualizationsButton]));a.append(this._render_secondaryActionButtons([this._render_tagButton,this._render_annotateButton]));a.append('
');a.append(this._render_tagArea());a.append(this._render_annotationArea());a.append(this._render_displayAppArea());this._render_displayApps(a);a.append(this._render_peek())},events:{"click .historyItemTitle":"toggleBodyVisibility","click a.icon-button.tags":"loadAndDisplayTags","click a.icon-button.annotate":"loadAndDisplayAnnotation"},loadAndDisplayTags:function(c){this.log(this+".loadAndDisplayTags",c);var a=this,d=this.$el.find(".tag-area"),b=d.find(".tag-elt");if(d.is(":hidden")){if(!jQuery.trim(b.html())){$.ajax({url:this.urls.tags.get,error:function(g,e,f){a.log("Tagging failed",g,e,f);a.trigger("error",_l("Tagging failed"),g,e,f)},success:function(e){b.html(e);b.find(".tooltip").tooltip();d.slideDown("fast")}})}else{d.slideDown("fast")}}else{d.slideUp("fast")}return false},loadAndDisplayAnnotation:function(b){this.log(this+".loadAndDisplayAnnotation",b);var d=this.$el.find(".annotation-area"),c=d.find(".annotation-elt"),a=this.urls.annotation.set;if(d.is(":hidden")){if(!jQuery.trim(c.html())){$.ajax({url:this.urls.annotation.get,error:function(){view.log("Annotation failed",xhr,status,error);view.trigger("error",_l("Annotation failed"),xhr,status,error)},success:function(e){if(e===""){e=""+_l("Describe or add notes to dataset")+""}c.html(e);d.find(".tooltip").tooltip();async_save_text(c.attr("id"),c.attr("id"),a,"new_annotation",18,true,4);d.slideDown("fast")}})}else{d.slideDown("fast")}}else{d.slideUp("fast")}return false},toString:function(){var a=(this.model)?(this.model+""):("(no model)");return"HDAView("+a+")"}});HDAEditView.templates={tagArea:Handlebars.templates["template-hda-tagArea"],annotationArea:Handlebars.templates["template-hda-annotationArea"]};function create_scatterplot_action_fn(a,b){action=function(){var d=$(window.parent.document).find("iframe#galaxy_main"),c=a+"/scatterplot?"+$.param(b);d.attr("src",c);$("div.popmenu-wrapper").remove();return false};return action}function create_trackster_action_fn(a,c,b){return function(){var d={};if(b){d["f-dbkey"]=b}$.ajax({url:a+"/list_tracks?"+$.param(d),dataType:"html",error:function(){alert(_l("Could not add this dataset to browser")+".")},success:function(e){var f=window.parent;f.show_modal(_l("View Data in a New or Saved Visualization"),"",{Cancel:function(){f.hide_modal()},"View in saved visualization":function(){f.show_modal(_l("Add Data to Saved Visualization"),e,{Cancel:function(){f.hide_modal()},"Add to visualization":function(){$(f.document).find("input[name=id]:checked").each(function(){var g=$(this).val();c.id=g;f.location=a+"/trackster?"+$.param(c)})}})},"View in new visualization":function(){f.location=a+"/trackster?"+$.param(c)}})}});return false}}; \ No newline at end of file diff --git a/static/scripts/packed/utils/config.js b/static/scripts/packed/utils/config.js index f9e3fd12db4..e9d120c39b0 100644 --- a/static/scripts/packed/utils/config.js +++ b/static/scripts/packed/utils/config.js @@ -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").appendTo(j);x.append($("