3 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/d847b6985330/
Changeset: d847b6985330
User: jmchilton
Date: 2014-02-20 16:37:17
Summary: Extend SecurityHelper to allow generation of different "kind"s of keys.
There has been talk about including the object's class into the hash when generating keys so the ids are not overlapping between model types. This change could allow that, but my immediate desire is to just to create single-purpose per job keys to allow external job running code to access files on behalf of a given job.
Affected #: 1 file
diff -r 9c673e0ea8919fea6941f24fdf6936686840bb71 -r d847b69853302d9a127c31c612dd19cc7ffa9f61 lib/galaxy/web/security/__init__.py
--- a/lib/galaxy/web/security/__init__.py
+++ b/lib/galaxy/web/security/__init__.py
@@ -1,3 +1,4 @@
+import collections
import os, os.path, logging
import pkg_resources
@@ -37,27 +38,32 @@
self.id_secret = config['id_secret']
self.id_cipher = Blowfish.new( self.id_secret )
- def encode_id( self, obj_id ):
+ per_kind_id_secret_base = config.get( 'per_kind_id_secret_base', self.id_secret )
+ self.id_ciphers_for_key = _cipher_cache( per_kind_id_secret_base )
+
+ def encode_id( self, obj_id, kind=None ):
+ id_cipher = self.__id_cipher( kind )
# Convert to string
s = str( obj_id )
# Pad to a multiple of 8 with leading "!"
s = ( "!" * ( 8 - len(s) % 8 ) ) + s
# Encrypt
- return self.id_cipher.encrypt( s ).encode( 'hex' )
+ return id_cipher.encrypt( s ).encode( 'hex' )
- def encode_dict_ids( self, a_dict ):
+ def encode_dict_ids( self, a_dict, kind=None ):
"""
Encode all ids in dictionary. Ids are identified by (a) an 'id' key or
(b) a key that ends with '_id'
"""
for key, val in a_dict.items():
if key == 'id' or key.endswith('_id'):
- a_dict[ key ] = self.encode_id( val )
+ a_dict[ key ] = self.encode_id( val, kind=kind )
return a_dict
- def decode_id( self, obj_id ):
- return int( self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) )
+ def decode_id( self, obj_id, kind=None ):
+ id_cipher = self.__id_cipher( kind )
+ return int( id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) )
def encode_guid( self, session_key ):
# Session keys are strings
@@ -73,3 +79,19 @@
def get_new_guid( self ):
# Generate a unique, high entropy 128 bit random number
return get_random_bytes( 16 )
+
+ def __id_cipher( self, kind ):
+ if not kind:
+ id_cipher = self.id_cipher
+ else:
+ id_cipher = self.id_ciphers_for_key[ kind ]
+ return id_cipher
+
+
+class _cipher_cache( collections.defaultdict ):
+
+ def __init__( self, secret_base ):
+ self.secret_base = secret_base
+
+ def __missing__( self, key ):
+ return Blowfish.new( self.secret_base + "__" + key )
https://bitbucket.org/galaxy/galaxy-central/commits/58acdb670e4b/
Changeset: 58acdb670e4b
User: jmchilton
Date: 2014-02-20 16:37:17
Summary: Implement API allowing access to a job's files.
Provides a mechanism for remote job execution mechanisms to read and write files on behalf of an "active" jobs.
For a variety of reasons simply providing access to datasets is insuccifient for this - there are working directory files needed for metadata calculation, inputs and outputs may be of intermediate form (task split files for instance), tool files, location files, etc.... Therefore this API endpoint provides access to a jobs view of these files.
Some attempt is made to verify the paths that are written to are valid for the supplied job (they either correspond to the output datasets of the job or the working directory of the job). Authorizing suchs paths for reading is much more difficult and left undone due to the unstructure and arbitrary nature of .loc files.
To implement this securely and with minimal configuration required - single-purpose "job_key"s are generated to authenticate these API calls. Each key allow usage of the API only for a single, active job.
Update LWR job runner to be able to leverage this API to stage files remotely via HTTP for "remote_transfer" actions (added recently to the LWR client).
Affected #: 6 files
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/jobs/runners/lwr.py
--- a/lib/galaxy/jobs/runners/lwr.py
+++ b/lib/galaxy/jobs/runners/lwr.py
@@ -28,6 +28,10 @@
NO_REMOTE_GALAXY_FOR_METADATA_MESSAGE = "LWR misconfiguration - LWR client configured to set metadata remotely, but remote LWR isn't properly configured with a galaxy_home directory."
NO_REMOTE_DATATYPES_CONFIG = "LWR client is configured to use remote datatypes configuration when setting metadata externally, but LWR is not configured with this information. Defaulting to datatypes_conf.xml."
+# Is there a good way to infer some default for this? Can only use
+# url_for from web threads. https://gist.github.com/jmchilton/9098762
+DEFAULT_GALAXY_URL = "http://localhost:8080"
+
class LwrJobRunner( AsynchronousJobRunner ):
"""
@@ -35,13 +39,14 @@
"""
runner_name = "LWRRunner"
- def __init__( self, app, nworkers, transport=None, cache=None, url=None ):
+ def __init__( self, app, nworkers, transport=None, cache=None, url=None, galaxy_url=DEFAULT_GALAXY_URL ):
"""Start the job runner """
super( LwrJobRunner, self ).__init__( app, nworkers )
self.async_status_updates = dict()
self._init_monitor_thread()
self._init_worker_threads()
client_manager_kwargs = {'transport_type': transport, 'cache': string_as_bool_or_none(cache), "url": url}
+ self.galaxy_url = galaxy_url
self.client_manager = build_client_manager(**client_manager_kwargs)
def url_to_destination( self, url ):
@@ -224,7 +229,21 @@
return self.get_client( job_destination_params, job_id )
def get_client( self, job_destination_params, job_id ):
- return self.client_manager.get_client( job_destination_params, str( job_id ) )
+ # Cannot use url_for outside of web thread.
+ #files_endpoint = url_for( controller="job_files", job_id=encoded_job_id )
+
+ encoded_job_id = self.app.security.encode_id(job_id)
+ job_key = self.app.security.encode_id( job_id, kind="jobs_files" )
+ files_endpoint = "%s/api/jobs/%s/files?job_key=%s" % (
+ self.galaxy_url,
+ encoded_job_id,
+ job_key
+ )
+ get_client_kwds = dict(
+ job_id=str( job_id ),
+ files_endpoint=files_endpoint,
+ )
+ return self.client_manager.get_client( job_destination_params, **get_client_kwds )
def finish_job( self, job_state ):
stderr = stdout = ''
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/model/__init__.py
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -266,6 +266,16 @@
self.handler = None
self.exit_code = None
+ @property
+ def finished( self ):
+ states = self.states
+ return self.state in [
+ states.OK,
+ states.ERROR,
+ states.DELETED,
+ states.DELETED_NEW,
+ ]
+
# TODO: Add accessors for members defined in SQL Alchemy for the Job table and
# for the mapper defined to the Job table.
def get_external_output_metadata( self ):
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/web/__init__.py
--- a/lib/galaxy/web/__init__.py
+++ b/lib/galaxy/web/__init__.py
@@ -20,3 +20,4 @@
from framework import _future_expose_api
from framework import _future_expose_api_anonymous
from framework import _future_expose_api_raw
+from framework import _future_expose_api_raw_anonymous
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/web/framework/__init__.py
--- a/lib/galaxy/web/framework/__init__.py
+++ b/lib/galaxy/web/framework/__init__.py
@@ -278,6 +278,10 @@
return _future_expose_api( func, to_json=False, user_required=True )
+def _future_expose_api_raw_anonymous( func ):
+ return _future_expose_api( func, to_json=False, user_required=False )
+
+
# TODO: rename as expose_api and make default.
def _future_expose_api( func, to_json=True, user_required=True ):
"""
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/webapps/galaxy/api/job_files.py
--- /dev/null
+++ b/lib/galaxy/webapps/galaxy/api/job_files.py
@@ -0,0 +1,142 @@
+""" API for asynchronous job running mechanisms can use to fetch or put files
+related to running and queued jobs.
+"""
+import os
+import shutil
+
+from galaxy import exceptions
+from galaxy import util
+from galaxy import model
+from galaxy.web.base.controller import BaseAPIController
+from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous
+from galaxy.web import _future_expose_api_raw_anonymous as expose_api_raw_anonymous
+
+
+import logging
+log = logging.getLogger( __name__ )
+
+
+class JobFilesAPIController( BaseAPIController ):
+ """ This job files controller allows remote job running mechanisms to
+ read and modify the current state of files for queued and running jobs.
+ It is certainly not meant to represent part of Galaxy's stable, user
+ facing API.
+
+ Furthermore, even if a user key corresponds to the user running the job,
+ it should not be accepted for authorization - this API allows access to
+ low-level unfiltered files and such authorization would break Galaxy's
+ security model for tool execution.
+ """
+
+ @expose_api_raw_anonymous
+ def index( self, trans, job_id, **kwargs ):
+ """
+ index( self, trans, job_id, **kwargs )
+ * GET /api/jobs/{job_id}/files
+ Get a file required to staging a job (proper datasets, extra inputs,
+ task-split inputs, working directory files).
+
+ :type job_id: str
+ :param job_id: encoded id string of the job
+ :type path: str
+ :param path: Path to file.
+ :type job_key: str
+ :param job_key: A key used to authenticate this request as acting on
+ behalf or a job runner for the specified job.
+ ..note:
+ This API method is intended only for consumption by job runners,
+ not end users.
+
+ :rtype: binary
+ :returns: contents of file
+ """
+ self.__authorize_job_access( trans, job_id, **kwargs )
+ path = kwargs.get("path", None)
+ return open(path, 'rb')
+
+ @expose_api_anonymous
+ def create( self, trans, job_id, payload, **kwargs ):
+ """
+ create( self, trans, job_id, payload, **kwargs )
+ * POST /api/jobs/{job_id}/files
+ Populate an output file (formal dataset, task split part, working
+ directory file (such as those related to metadata)). This should be
+ a multipart post with a 'file' parameter containing the contents of
+ the actual file to create.
+
+ :type job_id: str
+ :param job_id: encoded id string of the job
+ :type payload: dict
+ :param payload: dictionary structure containing::
+ 'job_key' = Key authenticating
+ 'path' = Path to file to create.
+
+ ..note:
+ This API method is intended only for consumption by job runners,
+ not end users.
+
+ :rtype: dict
+ :returns: an okay message
+ """
+ job = self.__authorize_job_access( trans, job_id, **payload )
+ path = payload.get( "path" )
+ self.__check_job_can_write_to_path( trans, job, path )
+
+ # Is this writing an unneeded file? Should this just copy in Python?
+ input_file = payload.get( "file", payload.get( "__file", None ) ).file
+ try:
+ shutil.copyfile( input_file.name, path )
+ finally:
+ input_file.close()
+ return {"message": "ok"}
+
+ def __authorize_job_access(self, trans, encoded_job_id, **kwargs):
+ for key in [ "path", "job_key" ]:
+ if key not in kwargs:
+ error_message = "Job files action requires a valid '%s'." % key
+ raise exceptions.ObjectAttributeMissingException( error_message )
+
+ job_id = trans.security.decode_id( encoded_job_id )
+ job_key = trans.security.encode_id( job_id, kind="jobs_files" )
+ if not util.safe_str_cmp( kwargs[ "job_key" ], job_key ):
+ raise exceptions.ItemAccessibilityException("Invalid job_key supplied.")
+
+ # Verify job is active. Don't update the contents of complete jobs.
+ job = trans.sa_session.query( model.Job ).get( job_id )
+ if job.finished:
+ error_message = "Attempting to read or modify the files of a job that has already completed."
+ raise exceptions.MessageException( error_message )
+ return job
+
+ def __check_job_can_write_to_path( self, trans, job, path ):
+ """ Verify an idealized job runner should actually be able to write to
+ the specified path - it must be a dataset output, a dataset "extra
+ file", or a some place in the working directory of this job.
+
+ Would like similar checks for reading the unstructured nature of loc
+ files make this very difficult. (See abandoned work here
+ https://gist.github.com/jmchilton/9103619.)
+ """
+ in_work_dir = self.__in_working_directory( job, path, trans.app )
+ if not in_work_dir and not self.__is_output_dataset_path( job, path ):
+ raise exceptions.ItemAccessibilityException("Job is not authorized to write to supplied path.")
+
+ def __is_output_dataset_path( self, job, path ):
+ """ Check if is an output path for this job or a file in the an
+ output's extra files path.
+ """
+ da_lists = [ job.output_datasets, job.output_library_datasets ]
+ for da_list in da_lists:
+ for job_dataset_association in da_list:
+ dataset = job_dataset_association.dataset
+ if not dataset:
+ continue
+ if os.path.abspath( dataset.file_name ) == os.path.abspath( path ):
+ return True
+ elif util.in_directory( path, dataset.extra_files_path ):
+ return True
+ return False
+
+ def __in_working_directory( self, job, path, app ):
+ working_directory = app.object_store.get_filename(job, base_dir='job_work', dir_only=True, extra_dir=str(job.id))
+ return util.in_directory( path, working_directory )
diff -r d847b69853302d9a127c31c612dd19cc7ffa9f61 -r 58acdb670e4b8ea9a35d3b19aaab1e66c5b75009 lib/galaxy/webapps/galaxy/buildapp.py
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -233,12 +233,20 @@
'permissions',
path_prefix='/api/libraries/:library_id',
parent_resources=dict( member_name='library', collection_name='libraries' ) )
-
- webapp.mapper.resource( 'job',
- 'jobs',
+
+ webapp.mapper.resource( 'job',
+ 'jobs',
path_prefix='/api' )
webapp.mapper.connect( 'job_search', '/api/jobs/search', controller='jobs', action='search', conditions=dict( method=['POST'] ) )
+ # Job files controllers. Only for consumption by remote job runners.
+ webapp.mapper.resource( 'file',
+ 'files',
+ controller="job_files",
+ name_prefix="job_",
+ path_prefix='/api/jobs/:job_id',
+ parent_resources=dict( member_name="job", collection_name="jobs")
+ )
_add_item_extended_metadata_controller( webapp,
name_prefix="library_dataset_",
https://bitbucket.org/galaxy/galaxy-central/commits/2727f1617161/
Changeset: 2727f1617161
User: jmchilton
Date: 2014-03-17 01:54:02
Summary: Merged in jmchilton/galaxy-central-fork-1 (pull request #327)
Implement job files API.
Affected #: 7 files
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/jobs/runners/lwr.py
--- a/lib/galaxy/jobs/runners/lwr.py
+++ b/lib/galaxy/jobs/runners/lwr.py
@@ -28,6 +28,10 @@
NO_REMOTE_GALAXY_FOR_METADATA_MESSAGE = "LWR misconfiguration - LWR client configured to set metadata remotely, but remote LWR isn't properly configured with a galaxy_home directory."
NO_REMOTE_DATATYPES_CONFIG = "LWR client is configured to use remote datatypes configuration when setting metadata externally, but LWR is not configured with this information. Defaulting to datatypes_conf.xml."
+# Is there a good way to infer some default for this? Can only use
+# url_for from web threads. https://gist.github.com/jmchilton/9098762
+DEFAULT_GALAXY_URL = "http://localhost:8080"
+
class LwrJobRunner( AsynchronousJobRunner ):
"""
@@ -35,13 +39,14 @@
"""
runner_name = "LWRRunner"
- def __init__( self, app, nworkers, transport=None, cache=None, url=None ):
+ def __init__( self, app, nworkers, transport=None, cache=None, url=None, galaxy_url=DEFAULT_GALAXY_URL ):
"""Start the job runner """
super( LwrJobRunner, self ).__init__( app, nworkers )
self.async_status_updates = dict()
self._init_monitor_thread()
self._init_worker_threads()
client_manager_kwargs = {'transport_type': transport, 'cache': string_as_bool_or_none(cache), "url": url}
+ self.galaxy_url = galaxy_url
self.client_manager = build_client_manager(**client_manager_kwargs)
def url_to_destination( self, url ):
@@ -224,7 +229,21 @@
return self.get_client( job_destination_params, job_id )
def get_client( self, job_destination_params, job_id ):
- return self.client_manager.get_client( job_destination_params, str( job_id ) )
+ # Cannot use url_for outside of web thread.
+ #files_endpoint = url_for( controller="job_files", job_id=encoded_job_id )
+
+ encoded_job_id = self.app.security.encode_id(job_id)
+ job_key = self.app.security.encode_id( job_id, kind="jobs_files" )
+ files_endpoint = "%s/api/jobs/%s/files?job_key=%s" % (
+ self.galaxy_url,
+ encoded_job_id,
+ job_key
+ )
+ get_client_kwds = dict(
+ job_id=str( job_id ),
+ files_endpoint=files_endpoint,
+ )
+ return self.client_manager.get_client( job_destination_params, **get_client_kwds )
def finish_job( self, job_state ):
stderr = stdout = ''
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/model/__init__.py
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -268,6 +268,16 @@
self.handler = None
self.exit_code = None
+ @property
+ def finished( self ):
+ states = self.states
+ return self.state in [
+ states.OK,
+ states.ERROR,
+ states.DELETED,
+ states.DELETED_NEW,
+ ]
+
# TODO: Add accessors for members defined in SQL Alchemy for the Job table and
# for the mapper defined to the Job table.
def get_external_output_metadata( self ):
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/web/__init__.py
--- a/lib/galaxy/web/__init__.py
+++ b/lib/galaxy/web/__init__.py
@@ -20,3 +20,4 @@
from framework import _future_expose_api
from framework import _future_expose_api_anonymous
from framework import _future_expose_api_raw
+from framework import _future_expose_api_raw_anonymous
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/web/framework/__init__.py
--- a/lib/galaxy/web/framework/__init__.py
+++ b/lib/galaxy/web/framework/__init__.py
@@ -278,6 +278,10 @@
return _future_expose_api( func, to_json=False, user_required=True )
+def _future_expose_api_raw_anonymous( func ):
+ return _future_expose_api( func, to_json=False, user_required=False )
+
+
# TODO: rename as expose_api and make default.
def _future_expose_api( func, to_json=True, user_required=True ):
"""
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/web/security/__init__.py
--- a/lib/galaxy/web/security/__init__.py
+++ b/lib/galaxy/web/security/__init__.py
@@ -1,3 +1,4 @@
+import collections
import os, os.path, logging
import pkg_resources
@@ -37,27 +38,32 @@
self.id_secret = config['id_secret']
self.id_cipher = Blowfish.new( self.id_secret )
- def encode_id( self, obj_id ):
+ per_kind_id_secret_base = config.get( 'per_kind_id_secret_base', self.id_secret )
+ self.id_ciphers_for_key = _cipher_cache( per_kind_id_secret_base )
+
+ def encode_id( self, obj_id, kind=None ):
+ id_cipher = self.__id_cipher( kind )
# Convert to string
s = str( obj_id )
# Pad to a multiple of 8 with leading "!"
s = ( "!" * ( 8 - len(s) % 8 ) ) + s
# Encrypt
- return self.id_cipher.encrypt( s ).encode( 'hex' )
+ return id_cipher.encrypt( s ).encode( 'hex' )
- def encode_dict_ids( self, a_dict ):
+ def encode_dict_ids( self, a_dict, kind=None ):
"""
Encode all ids in dictionary. Ids are identified by (a) an 'id' key or
(b) a key that ends with '_id'
"""
for key, val in a_dict.items():
if key == 'id' or key.endswith('_id'):
- a_dict[ key ] = self.encode_id( val )
+ a_dict[ key ] = self.encode_id( val, kind=kind )
return a_dict
- def decode_id( self, obj_id ):
- return int( self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) )
+ def decode_id( self, obj_id, kind=None ):
+ id_cipher = self.__id_cipher( kind )
+ return int( id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) )
def encode_guid( self, session_key ):
# Session keys are strings
@@ -73,3 +79,19 @@
def get_new_guid( self ):
# Generate a unique, high entropy 128 bit random number
return get_random_bytes( 16 )
+
+ def __id_cipher( self, kind ):
+ if not kind:
+ id_cipher = self.id_cipher
+ else:
+ id_cipher = self.id_ciphers_for_key[ kind ]
+ return id_cipher
+
+
+class _cipher_cache( collections.defaultdict ):
+
+ def __init__( self, secret_base ):
+ self.secret_base = secret_base
+
+ def __missing__( self, key ):
+ return Blowfish.new( self.secret_base + "__" + key )
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/webapps/galaxy/api/job_files.py
--- /dev/null
+++ b/lib/galaxy/webapps/galaxy/api/job_files.py
@@ -0,0 +1,142 @@
+""" API for asynchronous job running mechanisms can use to fetch or put files
+related to running and queued jobs.
+"""
+import os
+import shutil
+
+from galaxy import exceptions
+from galaxy import util
+from galaxy import model
+from galaxy.web.base.controller import BaseAPIController
+from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous
+from galaxy.web import _future_expose_api_raw_anonymous as expose_api_raw_anonymous
+
+
+import logging
+log = logging.getLogger( __name__ )
+
+
+class JobFilesAPIController( BaseAPIController ):
+ """ This job files controller allows remote job running mechanisms to
+ read and modify the current state of files for queued and running jobs.
+ It is certainly not meant to represent part of Galaxy's stable, user
+ facing API.
+
+ Furthermore, even if a user key corresponds to the user running the job,
+ it should not be accepted for authorization - this API allows access to
+ low-level unfiltered files and such authorization would break Galaxy's
+ security model for tool execution.
+ """
+
+ @expose_api_raw_anonymous
+ def index( self, trans, job_id, **kwargs ):
+ """
+ index( self, trans, job_id, **kwargs )
+ * GET /api/jobs/{job_id}/files
+ Get a file required to staging a job (proper datasets, extra inputs,
+ task-split inputs, working directory files).
+
+ :type job_id: str
+ :param job_id: encoded id string of the job
+ :type path: str
+ :param path: Path to file.
+ :type job_key: str
+ :param job_key: A key used to authenticate this request as acting on
+ behalf or a job runner for the specified job.
+ ..note:
+ This API method is intended only for consumption by job runners,
+ not end users.
+
+ :rtype: binary
+ :returns: contents of file
+ """
+ self.__authorize_job_access( trans, job_id, **kwargs )
+ path = kwargs.get("path", None)
+ return open(path, 'rb')
+
+ @expose_api_anonymous
+ def create( self, trans, job_id, payload, **kwargs ):
+ """
+ create( self, trans, job_id, payload, **kwargs )
+ * POST /api/jobs/{job_id}/files
+ Populate an output file (formal dataset, task split part, working
+ directory file (such as those related to metadata)). This should be
+ a multipart post with a 'file' parameter containing the contents of
+ the actual file to create.
+
+ :type job_id: str
+ :param job_id: encoded id string of the job
+ :type payload: dict
+ :param payload: dictionary structure containing::
+ 'job_key' = Key authenticating
+ 'path' = Path to file to create.
+
+ ..note:
+ This API method is intended only for consumption by job runners,
+ not end users.
+
+ :rtype: dict
+ :returns: an okay message
+ """
+ job = self.__authorize_job_access( trans, job_id, **payload )
+ path = payload.get( "path" )
+ self.__check_job_can_write_to_path( trans, job, path )
+
+ # Is this writing an unneeded file? Should this just copy in Python?
+ input_file = payload.get( "file", payload.get( "__file", None ) ).file
+ try:
+ shutil.copyfile( input_file.name, path )
+ finally:
+ input_file.close()
+ return {"message": "ok"}
+
+ def __authorize_job_access(self, trans, encoded_job_id, **kwargs):
+ for key in [ "path", "job_key" ]:
+ if key not in kwargs:
+ error_message = "Job files action requires a valid '%s'." % key
+ raise exceptions.ObjectAttributeMissingException( error_message )
+
+ job_id = trans.security.decode_id( encoded_job_id )
+ job_key = trans.security.encode_id( job_id, kind="jobs_files" )
+ if not util.safe_str_cmp( kwargs[ "job_key" ], job_key ):
+ raise exceptions.ItemAccessibilityException("Invalid job_key supplied.")
+
+ # Verify job is active. Don't update the contents of complete jobs.
+ job = trans.sa_session.query( model.Job ).get( job_id )
+ if job.finished:
+ error_message = "Attempting to read or modify the files of a job that has already completed."
+ raise exceptions.MessageException( error_message )
+ return job
+
+ def __check_job_can_write_to_path( self, trans, job, path ):
+ """ Verify an idealized job runner should actually be able to write to
+ the specified path - it must be a dataset output, a dataset "extra
+ file", or a some place in the working directory of this job.
+
+ Would like similar checks for reading the unstructured nature of loc
+ files make this very difficult. (See abandoned work here
+ https://gist.github.com/jmchilton/9103619.)
+ """
+ in_work_dir = self.__in_working_directory( job, path, trans.app )
+ if not in_work_dir and not self.__is_output_dataset_path( job, path ):
+ raise exceptions.ItemAccessibilityException("Job is not authorized to write to supplied path.")
+
+ def __is_output_dataset_path( self, job, path ):
+ """ Check if is an output path for this job or a file in the an
+ output's extra files path.
+ """
+ da_lists = [ job.output_datasets, job.output_library_datasets ]
+ for da_list in da_lists:
+ for job_dataset_association in da_list:
+ dataset = job_dataset_association.dataset
+ if not dataset:
+ continue
+ if os.path.abspath( dataset.file_name ) == os.path.abspath( path ):
+ return True
+ elif util.in_directory( path, dataset.extra_files_path ):
+ return True
+ return False
+
+ def __in_working_directory( self, job, path, app ):
+ working_directory = app.object_store.get_filename(job, base_dir='job_work', dir_only=True, extra_dir=str(job.id))
+ return util.in_directory( path, working_directory )
diff -r 6495ceccc87d467f73d76e21f801acc78934fc8f -r 2727f16171610e0e27420d780e9a5ed56f1908f5 lib/galaxy/webapps/galaxy/buildapp.py
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -261,6 +261,14 @@
path_prefix='/api' )
webapp.mapper.connect( 'job_search', '/api/jobs/search', controller='jobs', action='search', conditions=dict( method=['POST'] ) )
+ # Job files controllers. Only for consumption by remote job runners.
+ webapp.mapper.resource( 'file',
+ 'files',
+ controller="job_files",
+ name_prefix="job_",
+ path_prefix='/api/jobs/:job_id',
+ parent_resources=dict( member_name="job", collection_name="jobs")
+ )
_add_item_extended_metadata_controller( webapp,
name_prefix="library_dataset_",
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/6f0f77a940fc/
Changeset: 6f0f77a940fc
User: jeremy goecks
Date: 2014-03-15 18:26:03
Summary: Config.js: make copy of model to avoid by reference errors. Remove debug statements.
Affected #: 2 files
diff -r 9fd3b0fc3a70b40d8f35e72c4ef2513fdb03a852 -r 6f0f77a940fc4966ce5534a06866ae57899dd867 static/scripts/utils/config.js
--- a/static/scripts/utils/config.js
+++ b/static/scripts/utils/config.js
@@ -128,13 +128,10 @@
* and a saved_values dictionary.
*/
from_models_and_saved_values: function(models, saved_values) {
- // Update models with saved values.
+ // If there are saved values, copy models and update with saved values.
if (saved_values) {
- _.each(models, function(m) {
- if (saved_values[m.key]) {
- // Found saved value, so update model.
- m.value = saved_values[m.key];
- }
+ models = _.map(models, function(m) {
+ return _.extend({}, m, { value: saved_values[m.key] });
});
}
diff -r 9fd3b0fc3a70b40d8f35e72c4ef2513fdb03a852 -r 6f0f77a940fc4966ce5534a06866ae57899dd867 static/scripts/viz/trackster/tracks.js
--- a/static/scripts/viz/trackster/tracks.js
+++ b/static/scripts/viz/trackster/tracks.js
@@ -208,7 +208,6 @@
// -- Set up drawable configuration. --
this.config = config_mod.ConfigSettingCollection.from_models_and_saved_values(this.config_params, obj_dict.prefs);
- this.config.each(function(s) { console.log(s.id, s.get('value')) })
// If there's no saved name, use object name.
if (!this.config.get_value('name')) {
@@ -2688,7 +2687,6 @@
* Use from_dict to recreate object.
*/
to_dict: function() {
- console.log(this.config.to_key_value_dict());
return {
track_type: this.get_type(),
dataset: {
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/9fd3b0fc3a70/
Changeset: 9fd3b0fc3a70
User: davebgx
Date: 2014-03-14 21:27:57
Summary: Fix misquoted command for tool dependency environment variable inheritance.
Affected #: 1 file
diff -r 61a423fddc6b82f9bc626318fddbf5c31a967c90 -r 9fd3b0fc3a70b40d8f35e72c4ef2513fdb03a852 lib/tool_shed/galaxy_install/tool_dependencies/fabric_util.py
--- a/lib/tool_shed/galaxy_install/tool_dependencies/fabric_util.py
+++ b/lib/tool_shed/galaxy_install/tool_dependencies/fabric_util.py
@@ -462,7 +462,7 @@
inherited_env_var_name = env_var_value.split( '[' )[1].split( ']' )[0]
to_replace = '$ENV[%s]' % inherited_env_var_name
# Build a command line that outputs VARIABLE_NAME: <the value of the variable>.
- set_prior_environment_commands.append( 'echo "%s: $%s"' % ( inherited_env_var_name, inherited_env_var_name ) )
+ set_prior_environment_commands.append( 'echo %s: $%s' % ( inherited_env_var_name, inherited_env_var_name ) )
command = ' ; '.join( set_prior_environment_commands )
# Run the command and capture the output.
command_return = handle_command( app, tool_dependency, install_dir, command, return_output=True )
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/61a423fddc6b/
Changeset: 61a423fddc6b
User: carlfeberhard
Date: 2014-03-14 21:03:48
Summary: Visualization Framework: add an 'embeddable' flag to visualization config files indicating whether the visualization can reliably be rendered outside a page or iframe and within another page; pass the embeddable flag to the links rendered from the registry; (more information and example at https://bitbucket.org/carlfeberhard/galaxy-embedded-visualization-template/…)
Affected #: 2 files
diff -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 -r 61a423fddc6b82f9bc626318fddbf5c31a967c90 config/plugins/visualizations/visualization.dtd
--- a/config/plugins/visualizations/visualization.dtd
+++ b/config/plugins/visualizations/visualization.dtd
@@ -3,10 +3,14 @@
<!-- visualization
name: the title/display name of the visualization (e.g. 'Trackster', 'Fastq Stats', etc.) REQUIRED
disabled: if included (value does not matter), this attribute will prevent the visualization being loaded
+ embeddable: if included (value does not matter), indicates that this visualization can be rendered as a DOM
+ fragment and won't render to a full page when passed the variable 'embedded' in the query string.
+ DEFAULT false.
--><!ATTLIST visualization
name CDATA #REQUIRED
disabled CDATA #IMPLIED
+ embeddable CDATA #IMPLIED
><!ELEMENT description (#PCDATA)>
diff -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 -r 61a423fddc6b82f9bc626318fddbf5c31a967c90 lib/galaxy/visualization/registry.py
--- a/lib/galaxy/visualization/registry.py
+++ b/lib/galaxy/visualization/registry.py
@@ -211,11 +211,13 @@
url = self.get_visualization_url( trans, target_object, visualization_name, param_data )
display_name = visualization.config.get( 'name', None )
render_target = visualization.config.get( 'render_target', 'galaxy_main' )
+ embeddable = visualization.config.get( 'embeddable', False )
# remap some of these vars for direct use in ui.js, PopupMenu (e.g. text->html)
return {
- 'href' : url,
- 'html' : display_name,
- 'target': render_target
+ 'href' : url,
+ 'html' : display_name,
+ 'target' : render_target,
+ 'embeddable': embeddable
}
return None
@@ -394,6 +396,14 @@
if not returned[ 'name' ]:
raise ParsingException( 'visualization needs a name attribute' )
+ # record the embeddable flag - defaults to false
+ # this is a design by contract promise that the visualization can be rendered inside another page
+ # often by rendering only a DOM fragment. Since this is an advanced feature that requires a bit more
+ # work from the creator's side - it defaults to False
+ returned[ 'embeddable' ] = False
+ if 'embeddable' in xml_tree.attrib:
+ returned[ 'embeddable' ] = xml_tree.attrib.get( 'embeddable', False ) == 'true'
+
# a (for now) text description of what the visualization does
description = xml_tree.find( 'description' )
returned[ 'description' ] = description.text.strip() if description is not None else None
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/b8f9e583b040/
Changeset: b8f9e583b040
User: carlfeberhard
Date: 2014-03-14 18:44:26
Summary: Scatterplot: wind down current development, fix label assignment, flatten model config, style fixes
Affected #: 8 files
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/Gruntfile.js
--- a/config/plugins/visualizations/scatterplot/Gruntfile.js
+++ b/config/plugins/visualizations/scatterplot/Gruntfile.js
@@ -35,6 +35,7 @@
uglify: {
// uglify the concat single file directly into the static dir
options: {
+ // uncomment these to allow better source mapping during development
//mangle : false,
//beautify : true
},
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/src/handlebars/chartcontrol.handlebars
--- a/config/plugins/visualizations/scatterplot/src/handlebars/chartcontrol.handlebars
+++ b/config/plugins/visualizations/scatterplot/src/handlebars/chartcontrol.handlebars
@@ -34,13 +34,13 @@
<div data-config-key="X-axis-label"class="text-input form-input"><label for="X-axis-label">Re-label the X axis: </label>
- <input type="text" name="X-axis-label" id="X-axis-label" value="{{x.label}}" />
+ <input type="text" name="X-axis-label" id="X-axis-label" value="{{xLabel}}" /><p class="form-help help-text-small"></p></div><div data-config-key="Y-axis-label" class="text-input form-input"><label for="Y-axis-label">Re-label the Y axis: </label>
- <input type="text" name="Y-axis-label" id="Y-axis-label" value="{{y.label}}" />
+ <input type="text" name="Y-axis-label" id="Y-axis-label" value="{{yLabel}}" /><p class="form-help help-text-small"></p></div>
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/src/handlebars/datacontrol.handlebars
--- a/config/plugins/visualizations/scatterplot/src/handlebars/datacontrol.handlebars
+++ b/config/plugins/visualizations/scatterplot/src/handlebars/datacontrol.handlebars
@@ -17,8 +17,8 @@
</div><p class="help-text help-text-small">
- <b>Note</b>: If it can be determined from the dataset's filetype that column is not numeric, that
- column choice may be disabled for either the x or y axis.
+ <b>Note</b>: If it can be determined from the dataset's filetype that a column is not numeric,
+ that column choice may be disabled for either the x or y axis.
</p><button class="render-button btn btn-primary active">Draw</button>
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js
--- a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js
+++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js
@@ -1,5 +1,6 @@
/* =============================================================================
todo:
+ localize
import button(display), func(model) - when user doesn't match
Move margins into wid/hi calcs (so final svg dims are w/h)
Better separation of AJAX in scatterplot.js (maybe pass in function?)
@@ -141,18 +142,18 @@
/** tab content to control how the chart is rendered (data glyph size, chart size, etc.) */
_render_chartControls : function( $where ){
+//TODO: as controls on actual chart
$where = $where || this.$el;
var editor = this,
config = this.model.get( 'config' ),
$chartControls = $where.find( '#chart-control' );
-
+
// ---- skeleton/form for controls
$chartControls.html( ScatterplotConfigEditor.templates.chartControl( config ) );
//console.debug( '$chartControl:', $chartControls );
// ---- slider controls
// limits for controls (by control/chartConfig id)
- //TODO: as class attribute
var controlRanges = {
'datapointSize' : { min: 2, max: 10, step: 1 },
'width' : { min: 200, max: 800, step: 20 },
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/src/scatterplot-display.js
--- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js
+++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js
@@ -184,7 +184,7 @@
//console.debug( JSON.stringify( stats, null, ' ' ) );
var config = this.model.get( 'config' ),
$statsTable = this.$el.find( '.stats-display' ),
- xLabel = config.x.label, yLabel = config.y.label,
+ xLabel = config.xLabel, yLabel = config.yLabel,
$table = $( '<table/>' ).addClass( 'table' )
.append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' ))
.append( _.map( stats, function( stat, key ){
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/src/scatterplot.js
--- a/config/plugins/visualizations/scatterplot/src/scatterplot.js
+++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js
@@ -73,19 +73,19 @@
// .................................................................... axes
var axis = { x : {}, y : {} };
- //console.log( 'x.ticks:', config.x.ticks );
- //console.log( 'y.ticks:', config.y.ticks );
+ //console.log( 'xTicks:', config.xTicks );
+ //console.log( 'yTicks:', config.yTicks );
axis.x.fn = d3.svg.axis()
.orient( 'bottom' )
.scale( interpolaterFns.x )
- .ticks( config.x.ticks )
+ .ticks( config.xTicks )
// this will convert thousands -> k, millions -> M, etc.
.tickFormat( d3.format( 's' ) );
axis.y.fn = d3.svg.axis()
.orient( 'left' )
.scale( interpolaterFns.y )
- .ticks( config.y.ticks )
+ .ticks( config.yTicks )
.tickFormat( d3.format( 's' ) );
axis.x.g = content.append( 'g' )
@@ -103,8 +103,9 @@
var padding = 6;
// x-axis label
axis.x.label = svg.append( 'text' )
+ .attr( 'id', 'x-axis-label' )
.attr( 'class', 'axis-label' )
- .text( config.x.label )
+ .text( config.xLabel )
// align to the top-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-after-edge' )
@@ -117,8 +118,9 @@
// y-axis label
// place 4 pixels left of the axis.y.g left edge
axis.y.label = svg.append( 'text' )
+ .attr( 'id', 'y-axis-label' )
.attr( 'class', 'axis-label' )
- .text( config.y.label )
+ .text( config.yLabel )
// align to bottom-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-before-edge' )
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/static/scatterplot-edit.js
--- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js
+++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js
@@ -1,1 +1,1 @@
-function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.x.ticks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.y.ticks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("class","axis-label").text(b.x.label).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("class","axis-label").text(b.y.label).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).classed("highlight",!1).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="'+i((f=b.x,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="'+i((f=b.y,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function";return g+='<p class="help-text">\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<ul class="help-text" style="margin-left: 8px">\n <li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>\n <li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>\n <li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n </li>\n</ul>\n\n<div class="column-selection">\n <pre class="peek">',(f=c.peek)?f=f.call(b,{hash:{},data:e}):(f=b.peek,f=typeof f===h?f.apply(b):f),(f||0===f)&&(g+=f),g+='</pre>\n</div>\n\n<p class="help-text help-text-small">\n <b>Note</b>: If it can be determined from the dataset\'s filetype that column is not numeric, that\n column choice may be disabled for either the x or y axis.\n</p>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),this.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.log("dataset:",this.dataset),this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekControl({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-control.change",function(a,c){b.model.set("config",c)}).on("peek-control.rename",function(){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>",'<div class="left">','<div class="page-control"></div>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.x.label,e=b.y.label,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}});
\ No newline at end of file
+function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).classed("highlight",!1).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="',(f=c.xLabel)?f=f.call(b,{hash:{},data:e}):(f=b.xLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="',(f=c.yLabel)?f=f.call(b,{hash:{},data:e}):(f=b.yLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function";return g+='<p class="help-text">\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<ul class="help-text" style="margin-left: 8px">\n <li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>\n <li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>\n <li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n </li>\n</ul>\n\n<div class="column-selection">\n <pre class="peek">',(f=c.peek)?f=f.call(b,{hash:{},data:e}):(f=b.peek,f=typeof f===h?f.apply(b):f),(f||0===f)&&(g+=f),g+='</pre>\n</div>\n\n<p class="help-text help-text-small">\n <b>Note</b>: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n</p>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),this.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.log("dataset:",this.dataset),this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekControl({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-control.change",function(a,c){b.model.set("config",c)}).on("peek-control.rename",function(){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>",'<div class="left">','<div class="page-control"></div>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.xLabel,e=b.yLabel,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}});
\ No newline at end of file
diff -r 707ba3eee2abb828ac262e76c5b9fbbc6c85cd03 -r b8f9e583b04024a8e181f5c0ef149b5aad51dba7 config/plugins/visualizations/scatterplot/static/scatterplot.css
--- a/config/plugins/visualizations/scatterplot/static/scatterplot.css
+++ b/config/plugins/visualizations/scatterplot/static/scatterplot.css
@@ -5,10 +5,11 @@
margin-bottom: 16px;
overflow: auto;
background-color: #ebd9b2;
- padding : 8px 12px 8px 12px;
+ padding : 12px 12px 4px 12px;
}
.chart-header h2 {
+ margin-top: 0px;
margin-bottom: 4px;
}
@@ -63,7 +64,7 @@
.file-controls .btn {
height: 24px;
padding: 0px 10px 0px 10px;
- line-height: 24px;
+ line-height: 20px;
}
.file-controls .copy-btn {
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/3fb927653301/
Changeset: 3fb927653301
User: jmchilton
Date: 2014-03-14 14:42:10
Summary: Add documentation for using GALAXY_SLOTS with local runner.
Affected #: 1 file
diff -r e22d4f2b57a8c99cf5b48d36e0ca440934eaa569 -r 3fb927653301a0c06a0bf94f2b6bd71b3595ec0d job_conf.xml.sample_advanced
--- a/job_conf.xml.sample_advanced
+++ b/job_conf.xml.sample_advanced
@@ -57,6 +57,12 @@
should be executed on those remote resources.
--><destination id="local" runner="local"/>
+ <destination id="multicore_local" runner="local">
+ <param id="local_slots">4</param><!-- Specify GALAXY_SLOTS for local jobs. -->
+ <!-- Warning: Local slot count doesn't tie up additional worker threads, to prevent over
+ allocating machine define a second local runner with different name and fewer workers
+ to run this destination. -->
+ </destination><destination id="pbs" runner="pbs" tags="mycluster"/><destination id="pbs_longjobs" runner="pbs" tags="mycluster,longjobs"><!-- Define parameters that are native to the job runner plugin. -->
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.