From 17addb206fd1e5c3ed8f0c72577e27dcdf54fe2e Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Tue, 6 Aug 2013 15:02:10 -0400 Subject: [PATCH] Visualizations framework: PluginFramework class, serving static and template files from plugins --- .hgignore | 2 +- config/plugins/visualizations/README.txt | 34 +++ .../visualizations/visualization.dtd | 2 - config/visualizations/circster.xml.sample | 26 --- config/visualizations/phyloviz.xml.sample | 18 -- config/visualizations/scatterplot.xml.sample | 15 -- config/visualizations/sweepster.xml.sample | 25 -- config/visualizations/trackster.xml.sample | 29 --- lib/galaxy/app.py | 6 +- lib/galaxy/config.py | 6 +- lib/galaxy/visualization/registry.py | 122 +++++----- lib/galaxy/web/base/pluginframework.py | 216 ++++++++++++++++++ lib/galaxy/web/framework/__init__.py | 9 +- lib/galaxy/web/framework/middleware/static.py | 2 + lib/galaxy/webapps/galaxy/buildapp.py | 34 ++- .../galaxy/controllers/visualization.py | 11 +- static/scripts/mvc/dataset/hda-edit.js | 4 +- universe_wsgi.ini.sample | 6 +- 18 files changed, 356 insertions(+), 211 deletions(-) create mode 100644 config/plugins/visualizations/README.txt rename config/{ => plugins}/visualizations/visualization.dtd (99%) delete mode 100644 config/visualizations/circster.xml.sample delete mode 100644 config/visualizations/phyloviz.xml.sample delete mode 100644 config/visualizations/scatterplot.xml.sample delete mode 100644 config/visualizations/sweepster.xml.sample delete mode 100644 config/visualizations/trackster.xml.sample create mode 100644 lib/galaxy/web/base/pluginframework.py diff --git a/.hgignore b/.hgignore index e47109d73bc..bdcb4825403 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 -config/visualizations/*.xml +config/* static/welcome.html.* static/welcome.html diff --git a/config/plugins/visualizations/README.txt b/config/plugins/visualizations/README.txt new file mode 100644 index 00000000000..8f4a4c964e7 --- /dev/null +++ b/config/plugins/visualizations/README.txt @@ -0,0 +1,34 @@ +Custom visualization plugins +---------------------------- + +Visualizations can be added to your Galaxy instance by creating +sub-directories, templates, and static files here. + +Properly configured and written visualizations will be accessible to +the user when they click the 'visualizations' icon for a dataset +in their history panel. + +The framework must be enabled in your 'universe_wsgi.ini' file by +uncommenting (and having a valid path for) the +'visualizations_plugin_directory' entry. + +For more information, see http://wiki.galaxyproject.org/VisualizationsRegistry + + +Sub-directory structure +----------------------- + +In general, sub-directories should follow the pattern: + + my_visualization/ + config/ + my_visualization.xml + static/ + ... any static files the visualization needs (if any) + templates/ + ... any Mako templates the visualization needs + +The XML config file for a visualization plugin can be validated on the command +line using (from your plugin directory): + + xmllint my_visualization/config/my_visualization.xml --valid --noout diff --git a/config/visualizations/visualization.dtd b/config/plugins/visualizations/visualization.dtd similarity index 99% rename from config/visualizations/visualization.dtd rename to config/plugins/visualizations/visualization.dtd index 75722998007..d874f6fd77a 100644 --- a/config/visualizations/visualization.dtd +++ b/config/plugins/visualizations/visualization.dtd @@ -1,5 +1,3 @@ - - diff --git a/config/visualizations/circster.xml.sample b/config/visualizations/circster.xml.sample deleted file mode 100644 index cf15d7b66b1..00000000000 --- a/config/visualizations/circster.xml.sample +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - HistoryDatasetAssociation - data.Data - dataset_id - hda_ldda - - - LibraryDatasetDatasetAssociation - data.Data - dataset_id - hda_ldda - - - - id - dataset_id - hda_ldda - dbkey - - - _top - diff --git a/config/visualizations/phyloviz.xml.sample b/config/visualizations/phyloviz.xml.sample deleted file mode 100644 index a2a879ef800..00000000000 --- a/config/visualizations/phyloviz.xml.sample +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - HistoryDatasetAssociation - data.Newick - data.Nexus - dataset_id - - - - dataset_id - tree_index - - - _top - diff --git a/config/visualizations/scatterplot.xml.sample b/config/visualizations/scatterplot.xml.sample deleted file mode 100644 index 3ab023cb5bc..00000000000 --- a/config/visualizations/scatterplot.xml.sample +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - HistoryDatasetAssociation - tabular.Tabular - dataset_id - - - - dataset_id - - - diff --git a/config/visualizations/sweepster.xml.sample b/config/visualizations/sweepster.xml.sample deleted file mode 100644 index 9dd19178756..00000000000 --- a/config/visualizations/sweepster.xml.sample +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - HistoryDatasetAssociation - data.Data - dataset_id - hda_ldda - - - LibraryDatasetDatasetAssociation - data.Data - dataset_id - hda_ldda - - - - visualization - dataset_id - hda_ldda - - - _top - diff --git a/config/visualizations/trackster.xml.sample b/config/visualizations/trackster.xml.sample deleted file mode 100644 index 813fa97fbb2..00000000000 --- a/config/visualizations/trackster.xml.sample +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - HistoryDatasetAssociation - data.Data - dataset_id - hda_ldda - dbkey - - - LibraryDatasetDatasetAssociation - data.Data - dataset_id - hda_ldda - - - - id - dataset_id - genome_region - dbkey - - tracks - - _top - diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index da6b2a8318b..904750b5436 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -123,10 +123,8 @@ class UniverseApplication( object ): # Load genome indexer tool. load_genome_index_tools( self.toolbox ) # visualizations registry: associates resources with visualizations, controls how to render - self.visualizations_registry = None - if self.config.visualizations_config_directory: - self.visualizations_registry = VisualizationsRegistry( self.config.root, - self.config.visualizations_config_directory ) + self.visualizations_registry = VisualizationsRegistry.from_config( + self.config.visualizations_plugins_directory, self.config ) # 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 687f2ed2f6b..acb31da39da 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -291,8 +291,10 @@ 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 ) ) - # visualization registries config directory - self.visualizations_config_directory = kwargs.get( 'visualizations_config_directory', None ) + # PLUGINS: + self.plugin_frameworks = [] + # visualization framework + self.visualizations_plugins_directory = kwargs.get( 'visualizations_plugins_directory', None ) @property def sentry_dsn_public( self ): diff --git a/lib/galaxy/visualization/registry.py b/lib/galaxy/visualization/registry.py index 5e0b8593f4d..188f24cd586 100644 --- a/lib/galaxy/visualization/registry.py +++ b/lib/galaxy/visualization/registry.py @@ -12,6 +12,8 @@ from galaxy import util import galaxy.model from galaxy.web import url_for +from galaxy.web.base import pluginframework + import logging log = logging.getLogger( __name__ ) @@ -28,17 +30,16 @@ some confused vocabulary in docs, var names tests: anding, grouping, not has_dataprovider + user is admin data_sources: lists of add description element to visualization. -TESTS to add: - has dataprovider - user is admin +user_pref for ordering/ex/inclusion of particular visualizations """ # ------------------------------------------------------------------- the registry -class VisualizationsRegistry( object ): +class VisualizationsRegistry( pluginframework.PluginFramework ): """ Main responsibilities are: - testing if an object has a visualization that can be applied to it @@ -47,6 +48,7 @@ class VisualizationsRegistry( object ): - validating and parsing params into resources (based on a context) used in the visualization template """ + #: any built in visualizations that have their own render method in ctrls/visualization # 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 = [ @@ -55,57 +57,37 @@ class VisualizationsRegistry( object ): 'sweepster', 'phyloviz' ] - # where to search for visualiztion templates (relative to templates/webapps/galaxy) + #: where to search for visualiztion templates (relative to templates/webapps/galaxy) # this can be overridden individually in the config entries TEMPLATE_ROOT = 'visualization' + #: directories under plugin_directory that aren't plugins + non_plugin_directories = [ 'bler' ] def __str__( self ): - listings_keys_str = ','.join( self.listings.keys() ) if self.listings else '' - return 'VisualizationsRegistry(%s)' %( listings_keys_str ) + return 'VisualizationsRegistry(%s)' %( self.plugin_directory ) - def __init__( self, galaxy_root, configuration_filepath ): - # load the registry from the xml files located in configuration_filepath using the given parser - configuration_filepath = os.path.join( galaxy_root, configuration_filepath ) - self.configuration_filepath = self.check_conf_filepath( configuration_filepath ) - self.move_sample_conf_files() - self.load() + def __init__( self, registry_filepath, template_cache_dir ): + super( VisualizationsRegistry, self ).__init__( registry_filepath, template_cache_dir ) # what to use to parse query strings into resources/vars for the template self.resource_parser = ResourceParser() + log.debug( '%s loaded', str( self ) ) - def check_conf_filepath( self, configuration_filepath ): + def load_configuration( self ): """ - Checks for the existence of the given filepath. - :param configurarion_filepath: full filepath to the visualization config directory - :raises IOError: if the given directory doesn't exist - """ - if not os.path.exists( configuration_filepath ): - raise IOError( 'visualization configuration directory (%s) not found' %( configuration_filepath ) ) - return configuration_filepath + Builds the registry by parsing the `config/*.xml` files for every plugin + in ``get_plugin_directories`` and stores the results in ``self.listings``. - def move_sample_conf_files( self ): + ..note:: + This could be used to re-load a new configuration without restarting + the instance. """ - Copies any `*.xml.sample` files in `configuration_filepath` to - `.xml` files of the same names if no file with that name already exists. + try: + self.listings = VisualizationsConfigParser.parse( self.get_plugin_directories() ) - :returns: a list of the files moved - """ - files_moved = [] - for sample_file in glob.glob( os.path.join( self.configuration_filepath, '*.sample' ) ): - new_name = os.path.splitext( sample_file )[0] - if not os.path.exists( new_name ): - shutil.copy2( sample_file, new_name ) - files_moved.append( new_name ) - - 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 ) + except Exception, exc: + log.exception( 'Error parsing visualizations plugins %s', self.plugin_directory ) + raise def get_visualization( self, trans, visualization_name, target_object ): """ @@ -283,11 +265,11 @@ class VisualizationsConfigParser( object ): VALID_RENDER_LOCATIONS = [ 'galaxy_main', '_top', '_blank' ] @classmethod - def parse( cls, config_dir, debug=True ): + def parse( cls, plugin_directories, debug=False ): """ Static class interface. """ - return cls( debug ).parse_files( config_dir ) + return cls( debug ).parse_plugins( plugin_directories ) def __init__( self, debug=False ): self.debug = debug @@ -297,33 +279,45 @@ class VisualizationsConfigParser( object ): self.param_parser = ParamParser() self.param_modifier_parser = ParamModifierParser() - def parse_files( self, config_dir ): + def parse_plugins( self, plugin_directories ): """ - Parse each XML file in `config_dir` for visualizations config data. + Parses the config files for each plugin sub-dir in `base_path`. + + :param plugin_directories: a list of paths to enabled plugins. - If an error occurs while parsing a visualizations entry, it is skipped. :returns: registry data in dictionary form """ returned = {} - try: - for xml_filepath in glob.glob( os.path.join( config_dir, '*.xml' ) ): - try: - visualization_name, visualization = self.parse_file( xml_filepath ) - # skip vis' with parsing errors - don't shutdown the startup - except ParsingException, parse_exc: - log.error( 'Skipped visualization config "%s" due to parsing errors: %s', - xml_filepath, str( parse_exc ), exc_info=self.debug ) + for plugin_path in plugin_directories: + returned.update( self.parse_plugin( plugin_path ) ) + return returned - if visualization: - returned[ visualization_name ] = visualization - log.debug( 'Visualization config loaded for: %s', visualization_name ) + def parse_plugin( self, plugin_path ): + """ + Parses any XML files in ``/config``. - except Exception, exc: - log.error( 'Error parsing visualizations configuration directory %s: %s', - config_dir, str( exc ), exc_info=( not self.debug ) ) - #TODO: change when this framework is on by default - if self.debug: - raise + If an error occurs while parsing a visualizations entry, it is skipped. + :returns: registry data in dictionary form + ..note:: + assumes config files are in a 'config' sub-dir of each plugin + """ + returned = {} + + plugin_config_path = os.path.join( plugin_path, 'config' ) + if not os.path.isdir( plugin_config_path ): + return returned + + for xml_filepath in glob.glob( os.path.join( plugin_config_path, '*.xml' ) ): + try: + visualization_name, visualization = self.parse_file( xml_filepath ) + # skip vis' with parsing errors - don't shutdown the startup + except ParsingException, parse_exc: + log.error( 'Skipped visualization config "%s" due to parsing errors: %s', + xml_filepath, str( parse_exc ), exc_info=self.debug ) + + if visualization: + returned[ visualization_name ] = visualization + log.debug( 'Visualization config loaded for: %s', visualization_name ) return returned diff --git a/lib/galaxy/web/base/pluginframework.py b/lib/galaxy/web/base/pluginframework.py new file mode 100644 index 00000000000..0ead96e23fc --- /dev/null +++ b/lib/galaxy/web/base/pluginframework.py @@ -0,0 +1,216 @@ +""" +Base class for plugins - frameworks or systems that may: + * serve static content + * serve templated html + * have some configuration at startup +""" + +import os.path +import glob +import sys + +import pkg_resources +pkg_resources.require( 'MarkupSafe' ) +pkg_resources.require( 'Mako' ) +import mako + + +# ============================================================================= exceptions +class PluginFrameworkException( Exception ): + """Base exception for plugin frameworks. + """ + pass +class PluginFrameworkConfigException( PluginFrameworkException ): + """Exception for plugin framework configuration errors. + """ + pass +class PluginFrameworkStaticException( PluginFrameworkException ): + """Exception for plugin framework static directory set up errors. + """ + pass +class PluginFrameworkTemplateException( PluginFrameworkException ): + """Exception for plugin framework template directory + and template rendering errors. + """ + pass + + +# ============================================================================= base +class PluginFramework( object ): + """ + Plugins are files/directories living outside the Galaxy ``lib`` directory + that serve static files (css, js, images, etc.), use and serve mako templates, + and have some configuration to control the rendering. + + A plugin framework sets up all the above components. + """ + #: does the class need a config file(s) to be parsed? + has_config = True + #: does the class need static files served? + serves_static = True + #: does the class need template files served? + serves_templates = True + #TODO: allow plugin mako inheritance from existing ``/templates`` files + #uses_galaxy_templates = True + #TODO: possibly better as instance var (or a combo) + #: the directories in ``plugin_directory`` with basenames listed here will + #: be ignored for config, static, and templates + non_plugin_directories = [] + + # ------------------------------------------------------------------------- setup + @classmethod + def from_config( cls, config_plugin_directory, config ): + """ + Set up the framework based on data from some config object by: + * constructing it's absolute plugin_directory filepath + * getting a template_cache + * and appending itself to the config object's ``plugin_frameworks`` list + + .. note:: + precondition: config obj should have attributes: + root, template_cache, and (list) plugin_frameworks + """ + # currently called from (base) app.py - defined here to allow override if needed + if not config_plugin_directory: + return None + try: + full_plugin_filepath = os.path.join( config.root, config_plugin_directory ) + template_cache = config.template_cache if cls.serves_static else None + plugin = cls( full_plugin_filepath, template_cache ) + + config.plugin_frameworks.append( plugin ) + return plugin + + except PluginFrameworkException, plugin_exc: + log.exception( "Error loading framework %s. Skipping...", cls.__class__.__name__ ) + return None + + def __str__( self ): + return '%s(%s)' %( self.__class__.__name__, self.plugin_directory ) + + def __init__( self, plugin_directory, template_cache_dir=None, debug=False ): + if not os.path.isdir( plugin_directory ): + raise PluginFrameworkException( 'Framework plugin directory not found: %s, %s' + %( self.__class__.__name__, plugin_directory ) ) + # absolute (?) path + self.plugin_directory = plugin_directory + self.name = os.path.basename( self.plugin_directory ) + + if self.has_config: + self.load_configuration() + # set_up_static_urls will be called during the static middleware creation (if serves_static) + if self.serves_templates: + self.set_up_templates( template_cache_dir ) + + def get_plugin_directories( self ): + """ + Return the plugin directory paths for this plugin. + + Gets any directories within ``plugin_directory`` that are directories + themselves and whose ``basename`` is not in ``plugin_directory``. + """ + # could instead explicitly list on/off in master config file + for plugin_path in glob.glob( os.path.join( self.plugin_directory, '*' ) ): + if not os.path.isdir( plugin_path ): + continue + + if os.path.basename( plugin_path ) in self.non_plugin_directories: + continue + + yield plugin_path + + # ------------------------------------------------------------------------- config + def load_configuration( self ): + """ + Override to load some framework/plugin specifc configuration. + """ + # Abstract method + return True + + # ------------------------------------------------------------------------- serving static files + def get_static_urls_and_paths( self ): + """ + For each plugin, return a 2-tuple where the first element is a url path + to the plugin's static files and the second is a filesystem path to those + same files. + + Meant to be passed to a Static url map. + """ + url_and_paths = [] + # called during the static middleware creation (buildapp.py, wrap_in_static) + + # NOTE: this only searches for static dirs two levels deep (i.e. //static) + for plugin_path in self.get_plugin_directories(): + # that path is a plugin, search for subdirs named static in THAT dir + plugin_static_path = os.path.join( plugin_path, 'static' ) + if not os.path.isdir( plugin_static_path ): + continue + + # build a url for that static subdir and create a Static urlmap entry for it + plugin_name = os.path.splitext( os.path.basename( plugin_path ) )[0] + plugin_url = self.name + '/' + plugin_name + '/static' + url_and_paths.append( ( plugin_url, plugin_static_path ) ) + + return url_and_paths + + # ------------------------------------------------------------------------- templates + def set_up_templates( self, template_cache_dir ): + """ + Add a ``template_lookup`` attribute to the framework that can be passed + to the mako renderer to find templates. + """ + if not template_cache_dir: + raise PluginFrameworkTemplateException( 'Plugins that serve templates require a template_cache_dir' ) + self.template_lookup = self._create_mako_template_lookup( template_cache_dir, self._get_template_paths() ) + return self.template_lookup + + def _get_template_paths( self ): + """ + Get the paths that will be searched for templates. + """ + return [ self.plugin_directory ] + + def _create_mako_template_lookup( self, cache_dir, paths, collection_size=500, output_encoding='utf-8' ): + """ + Create a ``TemplateLookup`` with defaults. + """ + return mako.lookup.TemplateLookup( + directories = paths, + module_directory = cache_dir, + collection_size = collection_size, + output_encoding = output_encoding ) + + #TODO: do we want to remove trans and app from the plugin template context? + def fill_template( self, trans, template_filename, **kwargs ): + """ + Pass control over to trans and render the ``template_filename``. + """ + # defined here to be overridden + return trans.fill_template( template_filename, template_lookup=self.template_lookup, **kwargs ) + + def fill_template_with_plugin_imports( self, trans, template_filename, **kwargs ): + """ + Returns a rendered plugin template but allows importing modules from inside + the plugin directory within the template. + + ..example:: I.e. given this layout for a plugin: + bler/ + template/ + bler.mako + static/ + conifg/ + my_script.py + this version of `fill_template` allows `bler.mako` to call `import my_script`. + """ + try: + plugin_base_path = os.path.split( os.path.dirname( template_filename ) )[0] + plugin_path = os.path.join( self.plugin_directory, plugin_base_path ) + sys.path.append( plugin_path ) + filled_template = self.fill_template( trans, template_filename, **kwargs ) + + finally: + sys.path.remove( plugin_path ) + + return filled_template + + #TODO: could add plugin template helpers here diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 7eb98b13732..82b34131080 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -993,10 +993,13 @@ class GalaxyWebTransaction( base.DefaultWebTransaction ): searchList=[kwargs, self.template_context, dict(caller=self, t=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app)] ) return str( template ) - def fill_template_mako( self, filename, **kwargs ): - template = self.webapp.mako_template_lookup.get_template( filename ) + def fill_template_mako( self, filename, template_lookup=None, **kwargs ): + template_lookup = template_lookup or self.webapp.mako_template_lookup + template = template_lookup.get_template( filename ) template.output_encoding = 'utf-8' - data = dict( caller=self, t=self, trans=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app ) + + data = dict( caller=self, t=self, trans=self, h=helpers, util=util, + request=self.request, response=self.response, app=self.app ) data.update( self.template_context ) data.update( kwargs ) return template.render( **data ) diff --git a/lib/galaxy/web/framework/middleware/static.py b/lib/galaxy/web/framework/middleware/static.py index 5b581532bec..cbbc17c9816 100644 --- a/lib/galaxy/web/framework/middleware/static.py +++ b/lib/galaxy/web/framework/middleware/static.py @@ -12,9 +12,11 @@ from paste.httpheaders import ETAG from paste.urlparser import StaticURLParser class CacheableStaticURLParser( StaticURLParser ): + def __init__( self, directory, cache_seconds=None ): StaticURLParser.__init__( self, directory ) self.cache_seconds = cache_seconds + def __call__( self, environ, start_response ): path_info = environ.get('PATH_INFO', '') if not path_info: diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 675b2c23fd3..9ec41a2db0e 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -2,22 +2,26 @@ Provides factory methods to assemble the Galaxy web application """ -import logging, atexit -import os, os.path -import sys, warnings - -from galaxy.util import asbool +import sys +import os +import os.path +import atexit +import warnings +import glob from paste import httpexceptions import pkg_resources -log = logging.getLogger( __name__ ) - -from galaxy import util import galaxy.model import galaxy.model.mapping import galaxy.datatypes.registry import galaxy.web.framework +from galaxy import util +from galaxy.util import asbool + +import logging +log = logging.getLogger( __name__ ) + class GalaxyWebApplication( galaxy.web.framework.WebApplication ): pass @@ -181,7 +185,7 @@ def app_factory( global_conf, **kwargs ): if kwargs.get( 'middleware', True ): webapp = wrap_in_middleware( webapp, global_conf, **kwargs ) if asbool( kwargs.get( 'static_enabled', True ) ): - webapp = wrap_in_static( webapp, global_conf, **kwargs ) + webapp = wrap_in_static( webapp, global_conf, plugin_frameworks=app.config.plugin_frameworks, **kwargs ) if asbool(kwargs.get('pack_scripts', False)): pack_scripts() # Close any pooled database connections before forking @@ -323,7 +327,7 @@ def wrap_in_middleware( app, global_conf, **local_conf ): log.debug( "Enabling 'Request ID' middleware" ) return app -def wrap_in_static( app, global_conf, **local_conf ): +def wrap_in_static( app, global_conf, plugin_frameworks=None, **local_conf ): from paste.urlmap import URLMap from galaxy.web.framework.middleware.static import CacheableStaticURLParser as Static urlmap = URLMap() @@ -343,6 +347,16 @@ def wrap_in_static( app, global_conf, **local_conf ): urlmap["/static/style"] = Static( conf.get( "static_style_dir" ), cache_time ) urlmap["/favicon.ico"] = Static( conf.get( "static_favicon_dir" ), cache_time ) urlmap["/robots.txt"] = Static( conf.get( "static_robots_txt", 'static/robots.txt'), cache_time ) + + # wrap any static dirs for plugins + plugin_frameworks = plugin_frameworks or [] + for static_serving_framework in ( framework for framework in plugin_frameworks if framework.serves_static ): + # invert control to each plugin for finding their own static dirs + for plugin_url, plugin_static_path in static_serving_framework.get_static_urls_and_paths(): + plugin_url = '/plugins/' + plugin_url + urlmap[( plugin_url )] = Static( plugin_static_path, cache_time ) + log.debug( 'added url, path to static middleware: %s, %s', plugin_url, plugin_static_path ) + # URL mapper becomes the root webapp return urlmap diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index 0e963a693dd..57fe8a60a9c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -708,7 +708,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, # validate name vs. registry registry = trans.app.visualizations_registry if not registry: - raise HTTPNotFound( 'No visualization registry (possibly disabled in universe_wsgi.ini)') + 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? @@ -722,16 +722,15 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, 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 ) + template_path = registry_listing[ 'template' ] + returned = registry.fill_template( trans, template_path, + visualization_name=visualization_name, query_args=kwargs, + embedded=embedded, shared_vars={}, **resources ) #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 ) ) diff --git a/static/scripts/mvc/dataset/hda-edit.js b/static/scripts/mvc/dataset/hda-edit.js index 296cb8127df..f78894e0a6e 100644 --- a/static/scripts/mvc/dataset/hda-edit.js +++ b/static/scripts/mvc/dataset/hda-edit.js @@ -616,7 +616,7 @@ function create_scatterplot_action_fn( url, params ){ title : "Scatterplot", type : "url", content : url + '/scatterplot?' + $.param(params), - center : true + location : 'center' }); //TODO: this needs to go away @@ -699,4 +699,4 @@ function create_trackster_action_fn(vis_url, dataset_params, dbkey) { //============================================================================== //return { // HDAView : HDAView, -//};}); \ No newline at end of file +//};}); diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 48aa4adfb55..4c235982d71 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -174,10 +174,8 @@ paste.app_factory = galaxy.web.buildapp:app_factory # Galaxy. #datatypes_config_file = datatypes_conf.xml -# Visualizations config directory, where to look for individual visualization -# xml configuration files. Those files define how visualizations apply to -# particular data and how to pass them the necessary parameters -#visualizations_config_directory = config/visualizations +# Visualizations config directory: where to look for individual visualization plugins. +#visualizations_plugins_directory = config/plugins/visualizations # Each job is given a unique empty directory as its current working directory. # This option defines in what parent directory those directories will be