Visualizations registry: refactor the relationship between plugins, the visualization controller, rendering, and rendering saved visualizations, minor fixes; Extend testing; Scatterplot: fix baseUrl resolution when used in own window

This commit is contained in:
Carl Eberhard
2015-04-28 15:41:01 -04:00
parent 1cf6e64abf
commit ed36e625bb
6 changed files with 237 additions and 109 deletions
@@ -20,7 +20,8 @@ var ScatterplotDisplay = Backbone.View.extend({
var view = this,
config = this.model.get( 'config' ),
//TODO: very tied to datasets - should be generalized eventually
xhr = jQuery.getJSON(((typeof parent.galaxy_config.root == 'undefined') ? '/' : parent.galaxy_config.root) + 'api/datasets/' + this.dataset.id, {
baseUrl = ( window.parent && parent.galaxy_config )? parent.galaxy_config.root : '/',
xhr = jQuery.getJSON( baseUrl + 'api/datasets/' + this.dataset.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : config.pagination.perPage,
File diff suppressed because one or more lines are too long
+48 -56
View File
@@ -129,7 +129,7 @@ class VisualizationPlugin( pluginframework.Plugin, ServesStaticPluginMixin, Serv
self.config = config
base_url = context.get( 'base_url', '' )
self.base_url = '/'.join([ base_url, self.name ])
self.base_url = '/'.join([ base_url, self.name ]) if base_url else self.name
self._set_up_static_plugin()
@@ -139,87 +139,79 @@ class VisualizationPlugin( pluginframework.Plugin, ServesStaticPluginMixin, Serv
self.resource_parser = resource_parser.ResourceParser( app )
def render( self, trans=None, visualization=None, config=None, embedded=None, **kwargs ):
def render( self, trans=None, embedded=None, **kwargs ):
"""
Return the text of the rendered plugin webpage/fragment.
Render and return the text of the non-saved plugin webpage/fragment.
"""
if visualization:
return self._render_saved( visualization, config, trans=trans, embedded=embedded, **kwargs )
config = {}
context = self._default_context_vars( embedded=embedded, **kwargs )
return self._render( config, trans=trans, embedded=embedded, context=context, **kwargs )
# get the config for passing to the template from the kwargs dict, parsed using the plugin's params setting
config_from_kwargs = self._query_dict_to_config( trans, kwargs )
config = utils.OpenObject( **config_from_kwargs )
# further parse config to resources (models, etc.) used in template based on registry config
resources = self._query_dict_to_resources( trans, config )
# if a saved visualization, pass in the encoded visualization id or None if a new render
encoded_visualization_id = None
visualization_display_name = self.config[ 'name' ]
title = kwargs.get( 'title', None )
return self._fill_template( trans,
visualization_name=self.name,
visualization_display_name=visualization_display_name,
title=title,
def render_saved( self, visualization, config, trans=None, embedded=None, **kwargs ):
"""
Render and return the text of the plugin webpage/fragment using the
config/data of a saved visualization.
"""
context = self._default_context_vars( embedded=embedded, **kwargs )
# update any values that were loaded from the Visualization
context.update( dict(
title=visualization.latest_revision.title,
saved_visualization=visualization,
visualization_id=encoded_visualization_id,
visualization_id=trans.security.encode_id( visualization.id ),
))
return self._render( config, trans=trans, embedded=embedded, context=context, **kwargs )
embedded=embedded,
# ---- non-public
def _default_context_vars( self, **kwargs ):
"""
Meta variables passed to the template/renderer to describe the visualization
being rendered.
These are the defaults used when a saved visualization isn't present to
provide them.
"""
return dict(
visualization_name=self.name,
visualization_display_name=self.config[ 'name' ],
title=kwargs.get( 'title', None ),
saved_visualization=None,
visualization_id=None,
# NOTE: passing *unparsed* kwargs as query
query=kwargs,
# NOTE: 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
vars={},
config=config,
**resources
)
def _render_saved( self, visualization, config, trans=None, embedded=None, **kwargs ):
def _render( self, config, context, trans=None, embedded=None, **kwargs ):
"""
Return the text of the saved visualization.
Build/fetch the variables needed to render the visualization and call the renderer.
"""
config = config or {}
template_args = {}
# get the config for passing to the template from the kwargs dict, parsed using the plugin's params setting
config = config or {}
config_from_kwargs = self._query_dict_to_config( trans, kwargs )
config.update( config_from_kwargs )
config = utils.OpenObject( **config )
template_args[ 'config' ] = config
# further parse config to resources (models, etc.) used in template based on registry config
resources = self._query_dict_to_resources( trans, config )
template_args.update( resources )
# if a saved visualization, pass in the encoded visualization id or None if a new render
encoded_visualization_id = trans.security.encode_id( visualization.id )
# add any extra variables dealing with the visualization itself, saved visualizations, etc.
template_args.update( context )
visualization_display_name = self.config[ 'name' ]
title = visualization.latest_revision.title
template_args.update( embedded=embedded )
return self._fill_template( trans, **template_args )
return self._fill_template( trans,
visualization_name=self.name,
visualization_display_name=visualization_display_name,
title=title,
saved_visualization=visualization,
visualization_id=encoded_visualization_id,
embedded=embedded,
# NOTE: passing *unparsed* kwargs as query
query=kwargs,
# NOTE: 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
vars={},
config=config,
**resources
)
# ---------------- getting resources for visualization templates from link query strings
def _fill_template( self, trans, **kwargs ):
# NOTE: (mako specific) 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
kwargs.update( vars={} )
template_filename = self.config[ 'entry_point' ][ 'file' ]
return trans.fill_template( template_filename, template_lookup=self.template_lookup, **kwargs )
# ---------------- getting resources for visualization templates from link query strings
def _get_resource_params_and_modifiers( self ):
"""
Get params and modifiers for the given visualization as a 2-tuple.
@@ -8,6 +8,7 @@ import os
import weakref
from galaxy.web import url_for
import galaxy.exceptions
from galaxy.web.base import pluginframework
from galaxy.visualization.plugins import config_parser
@@ -114,6 +115,14 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ):
))
return plugin
def get_plugin( self, key ):
"""
Wrap to throw error if plugin not in registry.
"""
if key not in self.plugins:
raise galaxy.exceptions.ObjectNotFound( 'Unknown or invalid visualization: ' + key )
return self.plugins[ key ]
# -- building links to visualizations from objects --
def get_visualizations( self, trans, target_object ):
"""
@@ -716,38 +716,14 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio
help="A description of the visualization; annotation is shown alongside published visualizations."),
template="visualization/create.mako" )
#
# Visualizations.
#
# ------------------------- registry.
@web.expose
@web.require_login( "use Galaxy visualizations", use_panels=True )
def saved( self, trans, id=None, revision=None, type=None, config=None, title=None, **kwargs ):
"""
"""
DEFAULT_VISUALIZATION_NAME = 'Unnamed Visualization'
# post to saved in order to save a visualization
# TODO: re-route this one to clear up signature
if trans.request.method == 'POST':
if type is None or config is None:
return HTTPBadRequest( 'A visualization type and config are required to save a visualization' )
if isinstance( config, basestring ):
config = loads( config )
title = title or DEFAULT_VISUALIZATION_NAME
# TODO: allow saving to (updating) a specific revision - should be part of UsesVisualization
# TODO: would be easier if this returned the visualization directly
# check security if posting to existing visualization
if id is not None:
visualization = self.get_visualization( trans, id, check_ownership=True, check_accessible=False )
# ??: on not owner: error raised, but not returned (status = 200)
# TODO: there's no security check in save visualization (if passed an id)
returned = self.save_visualization( trans, config, type, id, title )
# redirect to GET to prevent annoying 'Do you want to post again?' dialog on page reload
render_url = web.url_for( controller='visualization', action='saved', id=returned.get( 'vis_id' ) )
return trans.response.send_redirect( render_url )
self._POST_to_saved( trans, id=id, revision=revision, type=type, config=config, title=title, **kwargs )
if id is None:
return HTTPBadRequest( 'A valid visualization id is required to load a visualization' )
@@ -760,11 +736,15 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio
# re-add title to kwargs for passing to render
if title:
kwargs[ 'title' ] = title
return self.render( trans, visualization.type, visualization, config=config, **kwargs )
plugin = self._get_plugin_from_registry( trans, visualization.type )
try:
return plugin.render_saved( visualization, config, trans=trans, **kwargs )
except Exception, exception:
self._handle_plugin_error( trans, visualization.type, exception )
@web.expose
@web.require_login( "use Galaxy visualizations", use_panels=True )
def render( self, trans, visualization_name, visualization=None, config=None, embedded=None, **kwargs ):
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)
@@ -772,32 +752,66 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio
URL: /visualization/show/{visualization_name}
"""
config = config or {}
# validate name vs. registry
registry = trans.app.visualizations_registry
if not registry:
raise HTTPNotFound( 'No visualization registry (possibly disabled in galaxy.ini)' )
if visualization_name not in registry.plugins:
raise HTTPNotFound( 'Unknown or invalid visualization: ' + visualization_name )
plugin = registry.plugins[ visualization_name ]
returned = None
plugin = self._get_plugin_from_registry( trans, visualization_name )
try:
returned = plugin.render( controller=self, trans=trans,
visualization=visualization, config=config, embedded=embedded, **kwargs )
return plugin.render( trans=trans, embedded=embedded, **kwargs )
except Exception, exception:
log.exception( 'error rendering visualization (%s): %s', visualization_name, str( exception ) )
if trans.debug:
raise
returned = trans.show_error_message(
"There was an error rendering the visualization. " +
"Contact your Galaxy administrator if the problem persists." +
"<br/>Details: " + str( exception ), use_panels=False )
self._handle_plugin_error( trans, visualization_name, exception )
return returned
def _POST_to_saved( self, trans, id=None, revision=None, type=None, config=None, title=None, **kwargs ):
"""
Save the visualiztion info (revision, type, config, title, etc.) to
the Visualization at `id` or to a new Visualization if `id` is None.
Uses POST/redirect/GET after a successful save, redirecting to GET.
"""
DEFAULT_VISUALIZATION_NAME = 'Unnamed Visualization'
# post to saved in order to save a visualization
if type is None or config is None:
return HTTPBadRequest( 'A visualization type and config are required to save a visualization' )
if isinstance( config, basestring ):
config = loads( config )
title = title or DEFAULT_VISUALIZATION_NAME
# TODO: allow saving to (updating) a specific revision - should be part of UsesVisualization
# TODO: would be easier if this returned the visualization directly
# check security if posting to existing visualization
if id is not None:
self.get_visualization( trans, id, check_ownership=True, check_accessible=False )
# ??: on not owner: error raised, but not returned (status = 200)
# TODO: there's no security check in save visualization (if passed an id)
returned = self.save_visualization( trans, config, type, id, title )
# redirect to GET to prevent annoying 'Do you want to post again?' dialog on page reload
render_url = web.url_for( controller='visualization', action='saved', id=returned.get( 'vis_id' ) )
return trans.response.send_redirect( render_url )
def _get_plugin_from_registry( self, trans, visualization_name ):
"""
Get the named plugin from the registry.
:raises HTTPNotFound: if registry has been turned off in config.
:raises HTTPNotFound: if visualization_name isn't a registered plugin.
"""
if not trans.app.visualizations_registry:
raise HTTPNotFound( 'No visualization registry (possibly disabled in galaxy.ini)' )
return trans.app.visualizations_registry.get_plugin( visualization_name )
def _handle_plugin_error( self, trans, visualization_name, exception ):
"""
Log, raise if debugging; log and show html message if not.
"""
log.exception( 'error rendering visualization (%s): %s', visualization_name, str( exception ) )
if trans.debug:
raise
return trans.show_error_message(
"There was an error rendering the visualization. " +
"Contact your Galaxy administrator if the problem persists." +
"<br/>Details: " + str( exception ), use_panels=False )
#
# Visualizations.
#
@web.expose
@web.require_login()
def trackster(self, trans, **kwargs):
@@ -833,7 +847,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio
app['viz_config'] = self.get_visualization_config( trans, vis )
# backup id
app['id'] = id;
app['id'] = id
# add dataset id
app['add_dataset'] = new_dataset_id
@@ -0,0 +1,112 @@
"""
"""
import os
import imp
import unittest
test_utils = imp.load_source( 'test_utils',
os.path.join( os.path.dirname( __file__), '..', 'unittest_utils', 'utility.py' ) )
import galaxy_mock
from galaxy.visualization.plugins import plugin as vis_plugin
from galaxy.visualization.plugins import resource_parser
# -----------------------------------------------------------------------------
glx_dir = test_utils.get_galaxy_root()
template_cache_dir = os.path.join( glx_dir, 'database', 'compiled_templates' )
addtional_templates_dir = os.path.join( glx_dir, 'config', 'plugins', 'visualizations', 'common', 'templates' )
vis_reg_path = 'config/plugins/visualizations'
config1 = """\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="scatterplot">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<template>scatterplot.mako</template>
</visualization>
"""
# -----------------------------------------------------------------------------
class VisualizationsPlugin_TestCase( unittest.TestCase ):
def test_default_init( self ):
"""
"""
vis_dir = galaxy_mock.MockDir({
'config' : {
'vis1.xml' : ''
},
'static' : {},
'templates' : {},
})
config = dict()
plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path,
'myvis', config )
self.assertEqual( plugin.name, 'myvis' )
self.assertEqual( plugin.path, vis_dir.root_path )
self.assertEqual( plugin.config, {} )
self.assertEqual( plugin.base_url, 'myvis' )
# static
self.assertTrue( plugin.serves_static )
self.assertEqual( plugin.static_path, vis_dir.root_path + '/static' )
self.assertEqual( plugin.static_url, 'myvis/static' )
# template
self.assertTrue( plugin.serves_templates )
self.assertEqual( plugin.template_path, vis_dir.root_path + '/templates' )
self.assertEqual( plugin.template_lookup.__class__.__name__, 'TemplateLookup' )
# resource parser
self.assertIsInstance( plugin.resource_parser, resource_parser.ResourceParser )
def test_init_with_context( self ):
"""
"""
vis_dir = galaxy_mock.MockDir({
'config' : {
'vis1.xml' : ''
},
'static' : {},
'templates' : {},
})
config = dict()
context = dict(
base_url='u/wot/m8',
template_cache_dir='template_cache',
additional_template_paths=[ 'one' ]
)
plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path,
'myvis', config, context=context )
self.assertEqual( plugin.base_url, 'u/wot/m8/myvis' )
# static
self.assertEqual( plugin.static_url, 'u/wot/m8/myvis/static' )
# template
self.assertEqual( plugin.template_lookup.__class__.__name__, 'TemplateLookup' )
def test_init_without_static_or_templates( self ):
"""
"""
vis_dir = galaxy_mock.MockDir({
'config' : {
'vis1.xml' : ''
}
})
plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path,
'myvis', dict() )
self.assertFalse( plugin.serves_static )
self.assertFalse( plugin.serves_templates )
# not sure what this would do, but...
# -----------------------------------------------------------------------------
# TODO: config parser tests (in separate file)
if __name__ == '__main__':
unittest.main()