galaxy-commits
Threads by month
- ----- 2026 -----
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2010 -----
- December
- November
- October
- September
- August
- July
- June
- May
- 15302 discussions
commit/galaxy-central: dannon: Merged in dan/galaxy-central-prs/stable (pull request #613)
by commits-noreply@bitbucket.org 16 Dec '14
by commits-noreply@bitbucket.org 16 Dec '14
16 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/7086b87d83a9/
Changeset: 7086b87d83a9
Branch: stable
User: dannon
Date: 2014-12-16 06:31:43+00:00
Summary: Merged in dan/galaxy-central-prs/stable (pull request #613)
[STABLE] Do not |h escape tool dependency error message, as it is escaped and formated by tool_shed.util.basic_util.to_html_string
Affected #: 1 file
diff -r b930c6d7157889fd7f3dc7393e7b8961875a5eb3 -r 7086b87d83a9092cbece0fec6f3e3ed03266be0c templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
--- a/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
+++ b/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
@@ -57,7 +57,7 @@
<td>${tool_dependency.version|h}</td><td>${tool_dependency.type|h}</td><td>${tool_dependency.status|h}</td>
- <td>${error_message|h}</td>
+ <td>${error_message}</td></tr>
%endfor
</table>
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
0
18 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/ff54ae664024/
Changeset: ff54ae664024
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Code de-dup in workflow controller.
Affected #: 1 file
diff -r 4f767bb3989f2ee9b7e3fdb47fa4cdf716205fc6 -r ff54ae664024274bb876d54930f6c3e8786f629d lib/galaxy/webapps/galaxy/controllers/workflow.py
--- a/lib/galaxy/webapps/galaxy/controllers/workflow.py
+++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py
@@ -666,12 +666,8 @@
encode it as a json string that can be read by the workflow editor
web interface.
"""
- user = trans.get_user()
- id = trans.security.decode_id( id )
trans.workflow_building_mode = True
- # Load encoded workflow from database
- stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
- assert stored.user == user
+ stored = self.get_stored_workflow( trans, id, check_ownership=True, check_accessible=False )
workflow = stored.latest_workflow
# Pack workflow data into a dictionary and return
data = {}
@@ -898,12 +894,8 @@
"""
Exports a workflow to myExperiment website.
"""
-
- # Load encoded workflow from database
- id = trans.security.decode_id( id )
trans.workflow_building_mode = True
- stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
- self.security_check( trans, stored, False, True )
+ stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True )
# Convert workflow to dict.
workflow_dict = self._workflow_to_dict( trans, stored )
https://bitbucket.org/galaxy/galaxy-central/commits/e89cb5a78e20/
Changeset: e89cb5a78e20
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Refactor helper out for detailed encoding of a workflow for API.
Affected #: 2 files
diff -r ff54ae664024274bb876d54930f6c3e8786f629d -r e89cb5a78e209b8a9c1209603a357b10194e377e lib/galaxy/webapps/galaxy/api/workflows.py
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -75,44 +75,8 @@
if trans.sa_session.query(trans.app.model.StoredWorkflowUserShareAssociation).filter_by(user=trans.user, stored_workflow=stored_workflow).count() == 0:
message = "Workflow is neither importable, nor owned by or shared with current user"
raise exceptions.ItemAccessibilityException( message )
-
- item = stored_workflow.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id } )
- item['url'] = url_for('workflow', id=id)
- item['owner'] = stored_workflow.user.username
latest_workflow = stored_workflow.latest_workflow
- inputs = {}
- for step in latest_workflow.steps:
- step_type = step.type
- if step_type in ['data_input', 'data_collection_input']:
- if step.tool_inputs and "name" in step.tool_inputs:
- label = step.tool_inputs['name']
- elif step_type == "data_input":
- label = "Input Dataset"
- elif step_type == "data_collection_input":
- label = "Input Dataset Collection"
- else:
- raise ValueError("Invalid step_type %s" % step_type)
- inputs[step.id] = {'label': label, 'value': ""}
- else:
- pass
- # Eventually, allow regular tool parameters to be inserted and modified at runtime.
- # p = step.get_required_parameters()
- item['inputs'] = inputs
- item['annotation'] = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, stored_workflow )
- steps = {}
- for step in latest_workflow.steps:
- steps[step.id] = {'id': step.id,
- 'type': step.type,
- 'tool_id': step.tool_id,
- 'tool_version': step.tool_version,
- 'annotation': self.get_item_annotation_str( trans.sa_session, stored_workflow.user, step ),
- 'tool_inputs': step.tool_inputs,
- 'input_steps': {}}
- for conn in step.input_connections:
- steps[step.id]['input_steps'][conn.input_name] = {'source_step': conn.output_step_id,
- 'step_output': conn.output_name}
- item['steps'] = steps
- return item
+ return self.__encode_workflow( trans, stored_workflow, latest_workflow )
@expose_api
def create(self, trans, payload, **kwd):
@@ -512,6 +476,44 @@
)
return self.__encode_invocation_step( trans, invocation_step )
+ def __encode_workflow( self, trans, stored_workflow, workflow ):
+ item = stored_workflow.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id } )
+ item['url'] = url_for('workflow', id=item['id'])
+ item['owner'] = stored_workflow.user.username
+ inputs = {}
+ for step in workflow.steps:
+ step_type = step.type
+ if step_type in ['data_input', 'data_collection_input']:
+ if step.tool_inputs and "name" in step.tool_inputs:
+ label = step.tool_inputs['name']
+ elif step_type == "data_input":
+ label = "Input Dataset"
+ elif step_type == "data_collection_input":
+ label = "Input Dataset Collection"
+ else:
+ raise ValueError("Invalid step_type %s" % step_type)
+ inputs[step.id] = {'label': label, 'value': ""}
+ else:
+ pass
+ # Eventually, allow regular tool parameters to be inserted and modified at runtime.
+ # p = step.get_required_parameters()
+ item['inputs'] = inputs
+ item['annotation'] = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, stored_workflow )
+ steps = {}
+ for step in workflow.steps:
+ steps[step.id] = {'id': step.id,
+ 'type': step.type,
+ 'tool_id': step.tool_id,
+ 'tool_version': step.tool_version,
+ 'annotation': self.get_item_annotation_str( trans.sa_session, stored_workflow.user, step ),
+ 'tool_inputs': step.tool_inputs,
+ 'input_steps': {}}
+ for conn in step.input_connections:
+ steps[step.id]['input_steps'][conn.input_name] = {'source_step': conn.output_step_id,
+ 'step_output': conn.output_name}
+ item['steps'] = steps
+ return item
+
def __encode_invocation_step( self, trans, invocation_step ):
return self.encode_all_ids(
trans,
diff -r ff54ae664024274bb876d54930f6c3e8786f629d -r e89cb5a78e209b8a9c1209603a357b10194e377e test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -181,6 +181,13 @@
def setUp( self ):
super( WorkflowsApiTestCase, self ).setUp()
+ def test_show_valid( self ):
+ workflow_id = self.workflow_populator.simple_workflow( "test_regular" )
+ show_response = self._get( "workflows/%s" % workflow_id )
+ workflow = show_response.json()
+ self._assert_looks_like_instance_workflow_representation( workflow )
+ assert len(workflow["steps"]) == 3
+
def test_show_invalid_key_is_400( self ):
show_response = self._get( "workflows/%s" % self._random_key() )
self._assert_status_code_is( show_response, 400 )
@@ -731,4 +738,25 @@
self._assert_status_code_is( show_response, 200 )
return show_response.json()
+ def _assert_looks_like_instance_workflow_representation(self, workflow):
+ self._assert_has_keys(
+ workflow,
+ 'url',
+ 'owner',
+ 'inputs',
+ 'annotation',
+ 'steps'
+ )
+ for step in workflow["steps"].values():
+ self._assert_has_keys(
+ step,
+ 'id',
+ 'type',
+ 'tool_id',
+ 'tool_version',
+ 'annotation',
+ 'tool_inputs',
+ 'input_steps',
+ )
+
RunJobsSummary = namedtuple('RunJobsSummary', ['history_id', 'workflow_id', 'inputs', 'jobs'])
https://bitbucket.org/galaxy/galaxy-central/commits/448c66ad21e0/
Changeset: 448c66ad21e0
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Move building workflows from dictionaries into new WorkflowContentsManager.
Going to build up a WorkflowContentsManager that is reponsible for slightly more complex operations involving step logic, etc... and keep the WorkflowsManager focused on CRUD-like and sharing operations.
Affected #: 2 files
diff -r e89cb5a78e209b8a9c1209603a357b10194e377e -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -1,7 +1,18 @@
+from __future__ import absolute_import
+
+from collections import namedtuple
+import json
+
from galaxy import model
from galaxy import exceptions
+from galaxy.model.item_attrs import UsesAnnotations
from galaxy.workflow import modules
+# For WorkflowContentManager
+from galaxy.util.sanitize_html import sanitize_html
+from galaxy.workflow.steps import attach_ordered_steps
+from galaxy.workflow.modules import module_factory
+
class WorkflowsManager( object ):
""" Handle CRUD type operaitons related to workflows. More interesting
@@ -103,3 +114,124 @@
).filter_by(
workflow_id=stored_workflow.latest_workflow_id
)
+
+
+CreatedWorkflow = namedtuple("CreatedWorkflow", ["stored_workflow", "missing_tools"])
+
+
+class WorkflowContentsManager(UsesAnnotations):
+
+ def build_workflow_from_dict(
+ self,
+ trans,
+ data,
+ source=None,
+ add_to_menu=False,
+ publish=False
+ ):
+ # Put parameters in workflow mode
+ trans.workflow_building_mode = True
+ # Create new workflow from incoming dict
+ workflow = model.Workflow()
+ # If there's a source, put it in the workflow name.
+ if source:
+ name = "%s (imported from %s)" % ( data['name'], source )
+ else:
+ name = data['name']
+ workflow.name = name
+ if 'uuid' in data:
+ workflow.uuid = data['uuid']
+ # Assume no errors until we find a step that has some
+ workflow.has_errors = False
+ # Create each step
+ steps = []
+ # The editor will provide ids for each step that we don't need to save,
+ # but do need to use to make connections
+ steps_by_external_id = {}
+ # Keep track of tools required by the workflow that are not available in
+ # the local Galaxy instance. Each tuple in the list of missing_tool_tups
+ # will be ( tool_id, tool_name, tool_version ).
+ missing_tool_tups = []
+ supplied_steps = data[ 'steps' ]
+ # Try to iterate through imported workflow in such a way as to
+ # preserve step order.
+ step_indices = supplied_steps.keys()
+ try:
+ step_indices = sorted( step_indices, key=int )
+ except ValueError:
+ # to defensive, were these ever or will they ever not be integers?
+ pass
+ # First pass to build step objects and populate basic values
+ for step_index in step_indices:
+ step_dict = supplied_steps[ step_index ]
+ # Create the model class for the step
+ step = model.WorkflowStep()
+ steps.append( step )
+ steps_by_external_id[ step_dict['id' ] ] = step
+ # FIXME: Position should be handled inside module
+ step.position = step_dict['position']
+ module = module_factory.from_dict( trans, step_dict, secure=False )
+ module.save_to_step( step )
+ if module.type == 'tool' and module.tool is None:
+ # A required tool is not available in the local Galaxy instance.
+ missing_tool_tup = ( step_dict[ 'tool_id' ], step_dict[ 'name' ], step_dict[ 'tool_version' ] )
+ if missing_tool_tup not in missing_tool_tups:
+ missing_tool_tups.append( missing_tool_tup )
+ # Save the entire step_dict in the unused config field, be parsed later
+ # when we do have the tool
+ step.config = json.dumps(step_dict)
+ if step.tool_errors:
+ workflow.has_errors = True
+ # Stick this in the step temporarily
+ step.temp_input_connections = step_dict['input_connections']
+ # Save step annotation.
+ annotation = step_dict[ 'annotation' ]
+ if annotation:
+ annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
+ self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
+ # Second pass to deal with connections between steps
+ for step in steps:
+ # Input connections
+ for input_name, conn_list in step.temp_input_connections.iteritems():
+ if not conn_list:
+ continue
+ if not isinstance(conn_list, list): # Older style singleton connection
+ conn_list = [conn_list]
+ for conn_dict in conn_list:
+ conn = model.WorkflowStepConnection()
+ conn.input_step = step
+ conn.input_name = input_name
+ conn.output_name = conn_dict['output_name']
+ conn.output_step = steps_by_external_id[ conn_dict['id'] ]
+ del step.temp_input_connections
+
+ # Order the steps if possible
+ attach_ordered_steps( workflow, steps )
+
+ # Connect up
+ stored = model.StoredWorkflow()
+ stored.name = workflow.name
+ workflow.stored_workflow = stored
+ stored.latest_workflow = workflow
+ stored.user = trans.user
+ stored.published = publish
+ if data[ 'annotation' ]:
+ annotation = sanitize_html( data[ 'annotation' ], 'utf-8', 'text/html' )
+ self.add_item_annotation( trans.sa_session, stored.user, stored, annotation )
+
+ # Persist
+ trans.sa_session.add( stored )
+ trans.sa_session.flush()
+
+ if add_to_menu:
+ if trans.user.stored_workflow_menu_entries is None:
+ trans.user.stored_workflow_menu_entries = []
+ menuEntry = model.StoredWorkflowMenuEntry()
+ menuEntry.stored_workflow = stored
+ trans.user.stored_workflow_menu_entries.append( menuEntry )
+ trans.sa_session.flush()
+
+ return CreatedWorkflow(
+ stored_workflow=stored,
+ missing_tools=missing_tool_tups
+ )
diff -r e89cb5a78e209b8a9c1209603a357b10194e377e -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 lib/galaxy/web/base/controller.py
--- a/lib/galaxy/web/base/controller.py
+++ b/lib/galaxy/web/base/controller.py
@@ -38,14 +38,13 @@
from galaxy.managers import api_keys
from galaxy.managers import tags
+from galaxy.managers import workflows
from galaxy.managers import base as managers_base
from galaxy.datatypes.metadata import FileParameter
from galaxy.tools.parameters import visit_input_values
from galaxy.tools.parameters.basic import DataToolParameter
from galaxy.tools.parameters.basic import DataCollectionToolParameter
-from galaxy.util.json import dumps
from galaxy.workflow.modules import ToolModule
-from galaxy.workflow.steps import attach_ordered_steps
from galaxy.util import validation
@@ -1576,110 +1575,15 @@
"""
Creates a workflow from a dict. Created workflow is stored in the database and returned.
"""
-
- # Put parameters in workflow mode
- trans.workflow_building_mode = True
- # Create new workflow from incoming dict
- workflow = model.Workflow()
- # If there's a source, put it in the workflow name.
- if source:
- name = "%s (imported from %s)" % ( data['name'], source )
- else:
- name = data['name']
- workflow.name = name
- if 'uuid' in data:
- workflow.uuid = data['uuid']
- # Assume no errors until we find a step that has some
- workflow.has_errors = False
- # Create each step
- steps = []
- # The editor will provide ids for each step that we don't need to save,
- # but do need to use to make connections
- steps_by_external_id = {}
- # Keep track of tools required by the workflow that are not available in
- # the local Galaxy instance. Each tuple in the list of missing_tool_tups
- # will be ( tool_id, tool_name, tool_version ).
- missing_tool_tups = []
- supplied_steps = data[ 'steps' ]
- # Try to iterate through imported workflow in such a way as to
- # preserve step order.
- step_indices = supplied_steps.keys()
- try:
- step_indices = sorted( step_indices, key=int )
- except ValueError:
- # to defensive, were these ever or will they ever not be integers?
- pass
- # First pass to build step objects and populate basic values
- for step_index in step_indices:
- step_dict = supplied_steps[ step_index ]
- # Create the model class for the step
- step = model.WorkflowStep()
- steps.append( step )
- steps_by_external_id[ step_dict['id' ] ] = step
- # FIXME: Position should be handled inside module
- step.position = step_dict['position']
- module = module_factory.from_dict( trans, step_dict, secure=False )
- module.save_to_step( step )
- if module.type == 'tool' and module.tool is None:
- # A required tool is not available in the local Galaxy instance.
- missing_tool_tup = ( step_dict[ 'tool_id' ], step_dict[ 'name' ], step_dict[ 'tool_version' ] )
- if missing_tool_tup not in missing_tool_tups:
- missing_tool_tups.append( missing_tool_tup )
- # Save the entire step_dict in the unused config field, be parsed later
- # when we do have the tool
- step.config = dumps(step_dict)
- if step.tool_errors:
- workflow.has_errors = True
- # Stick this in the step temporarily
- step.temp_input_connections = step_dict['input_connections']
- # Save step annotation.
- annotation = step_dict[ 'annotation' ]
- if annotation:
- annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
- # Second pass to deal with connections between steps
- for step in steps:
- # Input connections
- for input_name, conn_list in step.temp_input_connections.iteritems():
- if not conn_list:
- continue
- if not isinstance(conn_list, list): # Older style singleton connection
- conn_list = [conn_list]
- for conn_dict in conn_list:
- conn = model.WorkflowStepConnection()
- conn.input_step = step
- conn.input_name = input_name
- conn.output_name = conn_dict['output_name']
- conn.output_step = steps_by_external_id[ conn_dict['id'] ]
- del step.temp_input_connections
-
- # Order the steps if possible
- attach_ordered_steps( workflow, steps )
-
- # Connect up
- stored = model.StoredWorkflow()
- stored.name = workflow.name
- workflow.stored_workflow = stored
- stored.latest_workflow = workflow
- stored.user = trans.user
- stored.published = publish
- if data[ 'annotation' ]:
- annotation = sanitize_html( data[ 'annotation' ], 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, stored.user, stored, annotation )
-
- # Persist
- trans.sa_session.add( stored )
- trans.sa_session.flush()
-
- if add_to_menu:
- if trans.user.stored_workflow_menu_entries == None:
- trans.user.stored_workflow_menu_entries = []
- menuEntry = model.StoredWorkflowMenuEntry()
- menuEntry.stored_workflow = stored
- trans.user.stored_workflow_menu_entries.append( menuEntry )
- trans.sa_session.flush()
-
- return stored, missing_tool_tups
+ workflow_contents_manager = workflows.WorkflowContentsManager()
+ created_workflow = workflow_contents_manager.build_workflow_from_dict(
+ trans,
+ data,
+ source=source,
+ add_to_menu=add_to_menu,
+ publish=publish
+ )
+ return created_workflow.stored_workflow, created_workflow.missing_tools
def _workflow_to_dict( self, trans, stored ):
"""
https://bitbucket.org/galaxy/galaxy-central/commits/a81fd56c879d/
Changeset: a81fd56c879d
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Refactor updating workflow contents out of mixin into manager.
Introduce an API endpoint for this operation with test case.
Affected #: 5 files
diff -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -11,7 +11,7 @@
# For WorkflowContentManager
from galaxy.util.sanitize_html import sanitize_html
from galaxy.workflow.steps import attach_ordered_steps
-from galaxy.workflow.modules import module_factory
+from galaxy.workflow.modules import module_factory, is_tool_module_type
class WorkflowsManager( object ):
@@ -235,3 +235,86 @@
stored_workflow=stored,
missing_tools=missing_tool_tups
)
+
+ def update_workflow_from_dict(self, trans, stored_workflow, workflow_data, from_editor=False):
+ # Put parameters in workflow mode
+ trans.workflow_building_mode = True
+ # Convert incoming workflow data from json if coming from editor
+ data = json.loads(workflow_data) if from_editor else workflow_data
+ # Create new workflow from incoming data
+ workflow = model.Workflow()
+ # Just keep the last name (user can rename later)
+ workflow.name = stored_workflow.name
+ # Assume no errors until we find a step that has some
+ workflow.has_errors = False
+ # Create each step
+ steps = []
+ # The editor will provide ids for each step that we don't need to save,
+ # but do need to use to make connections
+ steps_by_external_id = {}
+ errors = []
+ for key, step_dict in data['steps'].iteritems():
+ is_tool = is_tool_module_type( step_dict[ 'type' ] )
+ if is_tool and step_dict['tool_id'] not in trans.app.toolbox.tools_by_id:
+ errors.append("Step %s requires tool '%s'." % (step_dict['id'], step_dict['tool_id']))
+ if errors:
+ raise MissingToolsException(workflow, errors)
+
+ # First pass to build step objects and populate basic values
+ for key, step_dict in data['steps'].iteritems():
+ # Create the model class for the step
+ step = model.WorkflowStep()
+ steps.append( step )
+ steps_by_external_id[ step_dict['id' ] ] = step
+ # FIXME: Position should be handled inside module
+ step.position = step_dict['position']
+ module = module_factory.from_dict( trans, step_dict, secure=from_editor )
+ module.save_to_step( step )
+ if 'workflow_outputs' in step_dict:
+ for output_name in step_dict['workflow_outputs']:
+ m = model.WorkflowOutput(workflow_step=step, output_name=output_name)
+ trans.sa_session.add(m)
+ if step.tool_errors:
+ # DBTODO Check for conditional inputs here.
+ workflow.has_errors = True
+ # Stick this in the step temporarily
+ step.temp_input_connections = step_dict['input_connections']
+ # Save step annotation.
+ annotation = step_dict[ 'annotation' ]
+ if annotation:
+ annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
+ self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
+ # Second pass to deal with connections between steps
+ for step in steps:
+ # Input connections
+ for input_name, conns in step.temp_input_connections.iteritems():
+ if conns:
+ conn_dicts = conns if isinstance(conns, list) else [ conns ]
+ for conn_dict in conn_dicts:
+ conn = model.WorkflowStepConnection()
+ conn.input_step = step
+ conn.input_name = input_name
+ conn.output_name = conn_dict['output_name']
+ conn.output_step = steps_by_external_id[ conn_dict['id'] ]
+ del step.temp_input_connections
+ # Order the steps if possible
+ attach_ordered_steps( workflow, steps )
+ # Connect up
+ workflow.stored_workflow = stored_workflow
+ stored_workflow.latest_workflow = workflow
+ # Persist
+ trans.sa_session.flush()
+ # Return something informative
+ errors = []
+ if workflow.has_errors:
+ errors.append( "Some steps in this workflow have validation errors" )
+ if workflow.has_cycles:
+ errors.append( "This workflow contains cycles" )
+ return workflow, errors
+
+
+class MissingToolsException(object):
+
+ def __init__(self, workflow, errors):
+ self.workflow = workflow
+ self.errors = errors
diff -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b lib/galaxy/web/base/controller.py
--- a/lib/galaxy/web/base/controller.py
+++ b/lib/galaxy/web/base/controller.py
@@ -1575,6 +1575,7 @@
"""
Creates a workflow from a dict. Created workflow is stored in the database and returned.
"""
+ # TODO: replace this method with direct access to manager.
workflow_contents_manager = workflows.WorkflowContentsManager()
created_workflow = workflow_contents_manager.build_workflow_from_dict(
trans,
diff -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b lib/galaxy/webapps/galaxy/api/workflows.py
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -271,6 +271,38 @@
"""
return self.__api_import_new_workflow( trans, payload, **kwd )
+ @expose_api
+ def update( self, trans, id, payload, **kwds ):
+ """
+ * PUT /api/workflows/{id}
+ updates the workflow stored with ``id``
+
+ :type id: str
+ :param id: the encoded id of the workflow to update
+ :type payload: dict
+ :param payload: a dictionary containing any or all the
+ * workflow the json description of the workflow as would be
+ produced by GET workflows/<id>/download or
+ given to `POST workflows`
+
+ The workflow contents will be updated to target
+ this.
+ :rtype: dict
+ :returns: serialized version of the workflow
+ """
+ stored_workflow = self.__get_stored_workflow( trans, id )
+ if 'workflow' in payload:
+ workflow_contents_manager = workflows.WorkflowContentsManager()
+ workflow, errors = workflow_contents_manager.update_workflow_from_dict(
+ trans,
+ stored_workflow,
+ payload['workflow'],
+ )
+ else:
+ message = "Updating workflow requires dictionary containing 'workflow' attribute with new JSON description."
+ raise exceptions.RequestParameterInvalidException( message )
+ return self.__encode_workflow( trans, stored_workflow, workflow )
+
def __api_import_new_workflow( self, trans, payload, **kwd ):
data = payload['workflow']
diff -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b lib/galaxy/webapps/galaxy/controllers/workflow.py
--- a/lib/galaxy/webapps/galaxy/controllers/workflow.py
+++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py
@@ -22,7 +22,8 @@
from galaxy.web import error, url_for
from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesStoredWorkflowMixin
from galaxy.web.framework.formbuilder import form
-from galaxy.web.framework.helpers import grids, time_ago, to_unicode
+from galaxy.web.framework.helpers import grids, time_ago, to_unicode, escape
+from galaxy.managers import workflows
from galaxy.workflow.modules import WorkflowModuleInjector
from galaxy.workflow.modules import MissingToolException
from galaxy.workflow.modules import module_factory, is_tool_module_type
@@ -796,76 +797,21 @@
"""
# Get the stored workflow
stored = self.get_stored_workflow( trans, id )
- # Put parameters in workflow mode
- trans.workflow_building_mode = True
- # Convert incoming workflow data from json
- data = json.loads( workflow_data )
- # Create new workflow from incoming data
- workflow = model.Workflow()
- # Just keep the last name (user can rename later)
- workflow.name = stored.name
- # Assume no errors until we find a step that has some
- workflow.has_errors = False
- # Create each step
- steps = []
- # The editor will provide ids for each step that we don't need to save,
- # but do need to use to make connections
- steps_by_external_id = {}
- errors = []
- for key, step_dict in data['steps'].iteritems():
- is_tool = is_tool_module_type( step_dict[ 'type' ] )
- if is_tool and step_dict['tool_id'] not in trans.app.toolbox.tools_by_id:
- errors.append("Step %s requires tool '%s'." % (step_dict['id'], step_dict['tool_id']))
- if errors:
- return dict( name=workflow.name,
- message="This workflow includes missing or invalid tools. It cannot be saved until the following steps are removed or the missing tools are enabled.",
- errors=errors )
- # First pass to build step objects and populate basic values
- for key, step_dict in data['steps'].iteritems():
- # Create the model class for the step
- step = model.WorkflowStep()
- steps.append( step )
- steps_by_external_id[ step_dict['id' ] ] = step
- # FIXME: Position should be handled inside module
- step.position = step_dict['position']
- module = module_factory.from_dict( trans, step_dict )
- module.save_to_step( step )
- if 'workflow_outputs' in step_dict:
- for output_name in step_dict['workflow_outputs']:
- m = model.WorkflowOutput(workflow_step=step, output_name=output_name)
- trans.sa_session.add(m)
- if step.tool_errors:
- # DBTODO Check for conditional inputs here.
- workflow.has_errors = True
- # Stick this in the step temporarily
- step.temp_input_connections = step_dict['input_connections']
- # Save step annotation.
- annotation = step_dict[ 'annotation' ]
- if annotation:
- annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
- # Second pass to deal with connections between steps
- for step in steps:
- # Input connections
- for input_name, conns in step.temp_input_connections.iteritems():
- if conns:
- conn_dicts = conns if isinstance(conns, list) else [ conns ]
- for conn_dict in conn_dicts:
- conn = model.WorkflowStepConnection()
- conn.input_step = step
- conn.input_name = input_name
- conn.output_name = conn_dict['output_name']
- conn.output_step = steps_by_external_id[ conn_dict['id'] ]
- del step.temp_input_connections
- # Order the steps if possible
- attach_ordered_steps( workflow, steps )
- # Connect up
- workflow.stored_workflow = stored
- stored.latest_workflow = workflow
- # Persist
- trans.sa_session.flush()
- # Return something informative
- errors = []
+ workflow_contents_manager = workflows.WorkflowContentsManager()
+ try:
+ workflow, errors = workflow_contents_manager.update_workflow_from_dict(
+ trans,
+ stored,
+ workflow_data,
+ from_editor=True,
+ )
+ except workflows.MissingToolsException as e:
+ return dict(
+ name=e.workflow.name,
+ message="This workflow includes missing or invalid tools. It cannot be saved until the following steps are removed or the missing tools are enabled.",
+ errors=e.errors,
+ )
+
if workflow.has_errors:
errors.append( "Some steps in this workflow have validation errors" )
if workflow.has_cycles:
diff -r 448c66ad21e012c7b18f1edfd7729527dc8cd507 -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -226,9 +226,9 @@
def test_upload_deprecated( self ):
self.__test_upload( use_deprecated_route=True )
- def __test_upload( self, use_deprecated_route ):
+ def __test_upload( self, use_deprecated_route=False, name="test_import" ):
data = dict(
- workflow=dumps( self.workflow_populator.load_workflow( name="test_import" ) ),
+ workflow=dumps( self.workflow_populator.load_workflow( name=name ) ),
)
if use_deprecated_route:
route = "workflows/upload"
@@ -236,7 +236,51 @@
route = "workflows"
upload_response = self._post( route, data=data )
self._assert_status_code_is( upload_response, 200 )
- self._assert_user_has_workflow_with_name( "test_import (imported from API)" )
+ self._assert_user_has_workflow_with_name( "%s (imported from API)" % name )
+ return upload_response
+
+ def test_update( self ):
+ original_workflow = self.workflow_populator.load_workflow( name="test_import" )
+
+ upload_response = self.__test_upload( )
+ workflow_id = upload_response.json()["id"]
+
+ def update(workflow_object):
+ data = dict(
+ workflow=workflow_object
+ )
+ raw_url = 'workflows/%s' % workflow_id
+ url = self._api_url( raw_url, use_key=True )
+ put_response = put( url, data=dumps(data) )
+ self._assert_status_code_is( put_response, 200 )
+ return put_response
+
+ workflow_content = self._download_workflow(workflow_id)
+ steps = workflow_content["steps"]
+
+ def tweak_step(step):
+ assert step['position']['top'] != 1
+ assert step['position']['left'] != 1
+ step['position'] = {'top': 1, 'left': 1}
+
+ map(tweak_step, steps.values())
+
+ update(workflow_content)
+
+ def check_step(step):
+ assert step['position']['top'] == 1
+ assert step['position']['left'] == 1
+
+ updated_workflow_content = self._download_workflow(workflow_id)
+ map(check_step, updated_workflow_content['steps'].values())
+
+ # Re-update against original worklfow...
+ update(original_workflow)
+
+ updated_workflow_content = self._download_workflow(workflow_id)
+
+ # Make sure the positions have been updated.
+ map(tweak_step, updated_workflow_content['steps'].values())
def test_import_deprecated( self ):
workflow_id = self.workflow_populator.simple_workflow( "test_import_published_deprecated", publish=True )
https://bitbucket.org/galaxy/galaxy-central/commits/2647ac82f98d/
Changeset: 2647ac82f98d
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Move body of _workflow_to_dict to new WorkflowContentsManager.
Getting closer to killing UsesStoredWorkflowMixin.
Affected #: 2 files
diff -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b -r 2647ac82f98dcd90a133be765122f97d6dede45e lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -11,7 +11,9 @@
# For WorkflowContentManager
from galaxy.util.sanitize_html import sanitize_html
from galaxy.workflow.steps import attach_ordered_steps
-from galaxy.workflow.modules import module_factory, is_tool_module_type
+from galaxy.workflow.modules import module_factory, is_tool_module_type, ToolModule
+from galaxy.tools.parameters.basic import DataToolParameter, DataCollectionToolParameter
+from galaxy.tools.parameters import visit_input_values
class WorkflowsManager( object ):
@@ -312,6 +314,108 @@
errors.append( "This workflow contains cycles" )
return workflow, errors
+ def workflow_to_dict(self, trans, stored, style="export" ):
+ """
+ """
+ workflow = stored.latest_workflow
+ workflow_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, stored )
+ annotation_str = ""
+ if workflow_annotation:
+ annotation_str = workflow_annotation.annotation
+ # Pack workflow data into a dictionary and return
+ data = {}
+ data['a_galaxy_workflow'] = 'true' # Placeholder for identifying galaxy workflow
+ data['format-version'] = "0.1"
+ data['name'] = workflow.name
+ data['annotation'] = annotation_str
+ if workflow.uuid is not None:
+ data['uuid'] = str(workflow.uuid)
+ data['steps'] = {}
+ # For each step, rebuild the form and encode the state
+ for step in workflow.steps:
+ # Load from database representation
+ module = module_factory.from_workflow_step( trans, step )
+ if not module:
+ return None
+ # Get user annotation.
+ step_annotation = self.get_item_annotation_obj(trans.sa_session, trans.user, step )
+ annotation_str = ""
+ if step_annotation:
+ annotation_str = step_annotation.annotation
+ # Step info
+ step_dict = {
+ 'id': step.order_index,
+ 'type': module.type,
+ 'tool_id': module.get_tool_id(),
+ 'tool_version': step.tool_version,
+ 'name': module.get_name(),
+ 'tool_state': module.get_state( secure=False ),
+ 'tool_errors': module.get_errors(),
+ ## 'data_inputs': module.get_data_inputs(),
+ ## 'data_outputs': module.get_data_outputs(),
+ 'annotation': annotation_str
+ }
+ # Add post-job actions to step dict.
+ if module.type == 'tool':
+ pja_dict = {}
+ for pja in step.post_job_actions:
+ pja_dict[pja.action_type + pja.output_name] = dict( action_type=pja.action_type,
+ output_name=pja.output_name,
+ action_arguments=pja.action_arguments )
+ step_dict[ 'post_job_actions' ] = pja_dict
+ # Data inputs
+ step_dict['inputs'] = module.get_runtime_input_dicts( annotation_str )
+ # User outputs
+ step_dict['user_outputs'] = []
+
+ # All step outputs
+ step_dict['outputs'] = []
+ if type( module ) is ToolModule:
+ for output in module.get_data_outputs():
+ step_dict['outputs'].append( { 'name': output['name'], 'type': output['extensions'][0] } )
+
+ # Connections
+ input_connections = step.input_connections
+ if step.type is None or step.type == 'tool':
+ # Determine full (prefixed) names of valid input datasets
+ data_input_names = {}
+
+ def callback( input, value, prefixed_name, prefixed_label ):
+ if isinstance( input, DataToolParameter ) or isinstance( input, DataCollectionToolParameter ):
+ data_input_names[ prefixed_name ] = True
+ # FIXME: this updates modules silently right now; messages from updates should be provided.
+ module.check_and_update_state()
+ visit_input_values( module.tool.inputs, module.state.inputs, callback )
+ # Filter
+ # FIXME: this removes connection without displaying a message currently!
+ input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
+
+ # Encode input connections as dictionary
+ input_conn_dict = {}
+ unique_input_names = set( [conn.input_name for conn in input_connections] )
+ for input_name in unique_input_names:
+ input_conn_dict[ input_name ] = \
+ [ dict( id=conn.output_step.order_index, output_name=conn.output_name ) for conn in input_connections if conn.input_name == input_name ]
+
+ # Preserve backward compatability. Previously Galaxy
+ # assumed input connections would be dictionaries not
+ # lists of dictionaries, so replace any singleton list
+ # with just the dictionary so that workflows exported from
+ # newer Galaxy instances can be used with older Galaxy
+ # instances if they do no include multiple input
+ # tools. This should be removed at some point. Mirrored
+ # hack in _workflow_from_dict should never be removed so
+ # existing workflow exports continue to function.
+ for input_name, input_conn in dict(input_conn_dict).iteritems():
+ if len(input_conn) == 1:
+ input_conn_dict[input_name] = input_conn[0]
+ step_dict['input_connections'] = input_conn_dict
+ # Position
+ step_dict['position'] = step.position
+ # Add to return value
+ data['steps'][step.order_index] = step_dict
+ return data
+
class MissingToolsException(object):
diff -r a81fd56c879dc517dd61e6bcd9bae0b9dd5bca7b -r 2647ac82f98dcd90a133be765122f97d6dede45e lib/galaxy/web/base/controller.py
--- a/lib/galaxy/web/base/controller.py
+++ b/lib/galaxy/web/base/controller.py
@@ -1590,111 +1590,11 @@
"""
Converts a workflow to a dict of attributes suitable for exporting.
"""
- workflow = stored.latest_workflow
- workflow_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, stored )
- annotation_str = ""
- if workflow_annotation:
- annotation_str = workflow_annotation.annotation
- # Pack workflow data into a dictionary and return
- data = {}
- data['a_galaxy_workflow'] = 'true' # Placeholder for identifying galaxy workflow
- data['format-version'] = "0.1"
- data['name'] = workflow.name
- data['annotation'] = annotation_str
- if workflow.uuid is not None:
- data['uuid'] = str(workflow.uuid)
- data['steps'] = {}
- # For each step, rebuild the form and encode the state
- for step in workflow.steps:
- # Load from database representation
- module = module_factory.from_workflow_step( trans, step )
- if not module:
- return None
- # Get user annotation.
- step_annotation = self.get_item_annotation_obj(trans.sa_session, trans.user, step )
- annotation_str = ""
- if step_annotation:
- annotation_str = step_annotation.annotation
- # Step info
- step_dict = {
- 'id': step.order_index,
- 'type': module.type,
- 'tool_id': module.get_tool_id(),
- 'tool_version' : step.tool_version,
- 'name': module.get_name(),
- 'tool_state': module.get_state( secure=False ),
- 'tool_errors': module.get_errors(),
- ## 'data_inputs': module.get_data_inputs(),
- ## 'data_outputs': module.get_data_outputs(),
- 'annotation' : annotation_str
- }
- # Add post-job actions to step dict.
- if module.type == 'tool':
- pja_dict = {}
- for pja in step.post_job_actions:
- pja_dict[pja.action_type+pja.output_name] = dict( action_type = pja.action_type,
- output_name = pja.output_name,
- action_arguments = pja.action_arguments )
- step_dict[ 'post_job_actions' ] = pja_dict
- # Data inputs
- step_dict['inputs'] = module.get_runtime_input_dicts( annotation_str )
- # User outputs
- step_dict['user_outputs'] = []
-# module_outputs = module.get_data_outputs()
-# step_outputs = trans.sa_session.query( WorkflowOutput ).filter( step=step )
-# for output in step_outputs:
-# name = output.output_name
-# annotation = ""
-# for module_output in module_outputs:
-# if module_output.get( 'name', None ) == name:
-# output_type = module_output.get( 'extension', '' )
-# break
-# data['outputs'][name] = { 'name' : name, 'annotation' : annotation, 'type' : output_type }
-
- # All step outputs
- step_dict['outputs'] = []
- if type( module ) is ToolModule:
- for output in module.get_data_outputs():
- step_dict['outputs'].append( { 'name' : output['name'], 'type' : output['extensions'][0] } )
- # Connections
- input_connections = step.input_connections
- if step.type is None or step.type == 'tool':
- # Determine full (prefixed) names of valid input datasets
- data_input_names = {}
-
- def callback( input, value, prefixed_name, prefixed_label ):
- if isinstance( input, DataToolParameter ) or isinstance( input, DataCollectionToolParameter ):
- data_input_names[ prefixed_name ] = True
- # FIXME: this updates modules silently right now; messages from updates should be provided.
- module.check_and_update_state()
- visit_input_values( module.tool.inputs, module.state.inputs, callback )
- # Filter
- # FIXME: this removes connection without displaying a message currently!
- input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
- # Encode input connections as dictionary
- input_conn_dict = {}
- unique_input_names = set( [conn.input_name for conn in input_connections] )
- for input_name in unique_input_names:
- input_conn_dict[ input_name ] = \
- [ dict( id=conn.output_step.order_index, output_name=conn.output_name ) for conn in input_connections if conn.input_name == input_name ]
- # Preserve backward compatability. Previously Galaxy
- # assumed input connections would be dictionaries not
- # lists of dictionaries, so replace any singleton list
- # with just the dictionary so that workflows exported from
- # newer Galaxy instances can be used with older Galaxy
- # instances if they do no include multiple input
- # tools. This should be removed at some point. Mirrored
- # hack in _workflow_from_dict should never be removed so
- # existing workflow exports continue to function.
- for input_name, input_conn in dict(input_conn_dict).iteritems():
- if len(input_conn) == 1:
- input_conn_dict[input_name] = input_conn[0]
- step_dict['input_connections'] = input_conn_dict
- # Position
- step_dict['position'] = step.position
- # Add to return value
- data['steps'][step.order_index] = step_dict
- return data
+ workflow_contents_manager = workflows.WorkflowContentsManager()
+ return workflow_contents_manager.workflow_to_dict(
+ trans,
+ stored,
+ )
class UsesFormDefinitionsMixin:
https://bitbucket.org/galaxy/galaxy-central/commits/b05becef6c64/
Changeset: b05becef6c64
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Move more workflow logic out of controller into manager.
This logic for building up editor representation of the workflow.
Introduce concept of a workflow to dict style - with 'export' and 'editor' as first cracks.
Affected #: 4 files
diff -r 2647ac82f98dcd90a133be765122f97d6dede45e -r b05becef6c64a4eb63bdc814a21243f2f5af799c lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -14,6 +14,7 @@
from galaxy.workflow.modules import module_factory, is_tool_module_type, ToolModule
from galaxy.tools.parameters.basic import DataToolParameter, DataCollectionToolParameter
from galaxy.tools.parameters import visit_input_values
+from galaxy.web import url_for
class WorkflowsManager( object ):
@@ -314,8 +315,143 @@
errors.append( "This workflow contains cycles" )
return workflow, errors
- def workflow_to_dict(self, trans, stored, style="export" ):
+ def workflow_to_dict( self, trans, stored, style="export" ):
+ """ Export the workflow contents to a dictionary ready for JSON-ification and to be
+ sent out via API for instance. There are two styles of export allowed 'editor' and 'export'.
+ The Galaxy team will do it best to preserve the backward compatibility of the 'export' stye -
+ but the 'editor' style is subject to rapid and unannounced changes.
"""
+ if style == "editor":
+ return self._workflow_to_dict_editor( trans, stored )
+ else:
+ return self._workflow_to_dict_export( trans, stored )
+
+ def _workflow_to_dict_editor(self, trans, stored):
+ """
+ """
+ workflow = stored.latest_workflow
+ # Pack workflow data into a dictionary and return
+ data = {}
+ data['name'] = workflow.name
+ data['steps'] = {}
+ data['upgrade_messages'] = {}
+ # For each step, rebuild the form and encode the state
+ for step in workflow.steps:
+ # Load from database representation
+ module = module_factory.from_workflow_step( trans, step )
+ if not module:
+ step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
+ annotation_str = ""
+ if step_annotation:
+ annotation_str = step_annotation.annotation
+ invalid_tool_form_html = """<div class="toolForm tool-node-error"><div class="toolFormTitle form-row-error">Unrecognized Tool: %s</div><div class="toolFormBody"><div class="form-row">
+ The tool id '%s' for this tool is unrecognized.<br/><br/>To save this workflow, you will need to delete this step or enable the tool.
+ </div></div></div>""" % (step.tool_id, step.tool_id)
+ step_dict = {
+ 'id': step.order_index,
+ 'type': 'invalid',
+ 'tool_id': step.tool_id,
+ 'name': 'Unrecognized Tool: %s' % step.tool_id,
+ 'tool_state': None,
+ 'tooltip': None,
+ 'tool_errors': ["Unrecognized Tool Id: %s" % step.tool_id],
+ 'data_inputs': [],
+ 'data_outputs': [],
+ 'form_html': invalid_tool_form_html,
+ 'annotation': annotation_str,
+ 'input_connections': {},
+ 'post_job_actions': {},
+ 'workflow_outputs': []
+ }
+ # Position
+ step_dict['position'] = step.position
+ # Add to return value
+ data['steps'][step.order_index] = step_dict
+ continue
+ # Fix any missing parameters
+ upgrade_message = module.check_and_update_state()
+ if upgrade_message:
+ # FIXME: Frontend should be able to handle workflow messages
+ # as a dictionary not just the values
+ data['upgrade_messages'][step.order_index] = upgrade_message.values()
+ # Get user annotation.
+ step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
+ annotation_str = ""
+ if step_annotation:
+ annotation_str = step_annotation.annotation
+ # Pack attributes into plain dictionary
+ step_dict = {
+ 'id': step.order_index,
+ 'type': module.type,
+ 'tool_id': module.get_tool_id(),
+ 'name': module.get_name(),
+ 'tool_state': module.get_state(),
+ 'tooltip': module.get_tooltip( static_path=url_for( '/static' ) ),
+ 'tool_errors': module.get_errors(),
+ 'data_inputs': module.get_data_inputs(),
+ 'data_outputs': module.get_data_outputs(),
+ 'form_html': module.get_config_form(),
+ 'annotation': annotation_str,
+ 'post_job_actions': {},
+ 'workflow_outputs': []
+ }
+ # Connections
+ input_connections = step.input_connections
+ input_connections_type = {}
+ multiple_input = {} # Boolean value indicating if this can be mutliple
+ if step.type is None or step.type == 'tool':
+ # Determine full (prefixed) names of valid input datasets
+ data_input_names = {}
+
+ def callback( input, value, prefixed_name, prefixed_label ):
+ if isinstance( input, DataToolParameter ) or isinstance( input, DataCollectionToolParameter ):
+ data_input_names[ prefixed_name ] = True
+ multiple_input[ prefixed_name ] = input.multiple
+ if isinstance( input, DataToolParameter ):
+ input_connections_type[ input.name ] = "dataset"
+ if isinstance( input, DataCollectionToolParameter ):
+ input_connections_type[ input.name ] = "dataset_collection"
+ visit_input_values( module.tool.inputs, module.state.inputs, callback )
+ # Filter
+ # FIXME: this removes connection without displaying a message currently!
+ input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
+ # post_job_actions
+ pja_dict = {}
+ for pja in step.post_job_actions:
+ pja_dict[pja.action_type + pja.output_name] = dict(
+ action_type=pja.action_type,
+ output_name=pja.output_name,
+ action_arguments=pja.action_arguments
+ )
+ step_dict['post_job_actions'] = pja_dict
+ #workflow outputs
+ outputs = []
+ for output in step.workflow_outputs:
+ outputs.append(output.output_name)
+ step_dict['workflow_outputs'] = outputs
+ # Encode input connections as dictionary
+ input_conn_dict = {}
+ for conn in input_connections:
+ input_type = "dataset"
+ if conn.input_name in input_connections_type:
+ input_type = input_connections_type[ conn.input_name ]
+ conn_dict = dict( id=conn.output_step.order_index, output_name=conn.output_name, input_type=input_type )
+ if conn.input_name in multiple_input:
+ if conn.input_name in input_conn_dict:
+ input_conn_dict[ conn.input_name ].append( conn_dict )
+ else:
+ input_conn_dict[ conn.input_name ] = [ conn_dict ]
+ else:
+ input_conn_dict[ conn.input_name ] = conn_dict
+ step_dict['input_connections'] = input_conn_dict
+ # Position
+ step_dict['position'] = step.position
+ # Add to return value
+ data['steps'][step.order_index] = step_dict
+ return data
+
+ def _workflow_to_dict_export( self, trans, stored ):
+ """ Export the workflow contents to a dictionary ready for JSON-ification and export.
"""
workflow = stored.latest_workflow
workflow_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, stored )
diff -r 2647ac82f98dcd90a133be765122f97d6dede45e -r b05becef6c64a4eb63bdc814a21243f2f5af799c lib/galaxy/webapps/galaxy/api/workflows.py
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -28,6 +28,7 @@
super( BaseAPIController, self ).__init__( app )
self.history_manager = histories.HistoryManager()
self.workflow_manager = workflows.WorkflowsManager( app )
+ self.workflow_contents_manager = workflows.WorkflowContentsManager()
@expose_api
def index(self, trans, **kwd):
@@ -221,7 +222,8 @@
"""
stored_workflow = self.__get_stored_accessible_workflow( trans, workflow_id )
- ret_dict = self._workflow_to_dict( trans, stored_workflow )
+ style = kwd.get("style", "export")
+ ret_dict = self.workflow_contents_manager.workflow_to_dict( trans, stored_workflow, style=style )
if not ret_dict:
# This workflow has a tool that's missing from the distribution
message = "Workflow cannot be exported due to missing tools."
diff -r 2647ac82f98dcd90a133be765122f97d6dede45e -r b05becef6c64a4eb63bdc814a21243f2f5af799c lib/galaxy/webapps/galaxy/controllers/workflow.py
--- a/lib/galaxy/webapps/galaxy/controllers/workflow.py
+++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py
@@ -669,126 +669,8 @@
"""
trans.workflow_building_mode = True
stored = self.get_stored_workflow( trans, id, check_ownership=True, check_accessible=False )
- workflow = stored.latest_workflow
- # Pack workflow data into a dictionary and return
- data = {}
- data['name'] = workflow.name
- data['steps'] = {}
- data['upgrade_messages'] = {}
- # For each step, rebuild the form and encode the state
- for step in workflow.steps:
- # Load from database representation
- module = module_factory.from_workflow_step( trans, step )
- if not module:
- step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
- annotation_str = ""
- if step_annotation:
- annotation_str = step_annotation.annotation
- invalid_tool_form_html = """<div class="toolForm tool-node-error"><div class="toolFormTitle form-row-error">Unrecognized Tool: %s</div><div class="toolFormBody"><div class="form-row">
- The tool id '%s' for this tool is unrecognized.<br/><br/>To save this workflow, you will need to delete this step or enable the tool.
- </div></div></div>""" % (step.tool_id, step.tool_id)
- step_dict = {
- 'id': step.order_index,
- 'type': 'invalid',
- 'tool_id': step.tool_id,
- 'name': 'Unrecognized Tool: %s' % step.tool_id,
- 'tool_state': None,
- 'tooltip': None,
- 'tool_errors': ["Unrecognized Tool Id: %s" % step.tool_id],
- 'data_inputs': [],
- 'data_outputs': [],
- 'form_html': invalid_tool_form_html,
- 'annotation': annotation_str,
- 'input_connections': {},
- 'post_job_actions': {},
- 'workflow_outputs': []
- }
- # Position
- step_dict['position'] = step.position
- # Add to return value
- data['steps'][step.order_index] = step_dict
- continue
- # Fix any missing parameters
- upgrade_message = module.check_and_update_state()
- if upgrade_message:
- # FIXME: Frontend should be able to handle workflow messages
- # as a dictionary not just the values
- data['upgrade_messages'][step.order_index] = upgrade_message.values()
- # Get user annotation.
- step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
- annotation_str = ""
- if step_annotation:
- annotation_str = step_annotation.annotation
- # Pack attributes into plain dictionary
- step_dict = {
- 'id': step.order_index,
- 'type': module.type,
- 'tool_id': module.get_tool_id(),
- 'name': module.get_name(),
- 'tool_state': module.get_state(),
- 'tooltip': module.get_tooltip( static_path=url_for( '/static' ) ),
- 'tool_errors': module.get_errors(),
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'form_html': module.get_config_form(),
- 'annotation': annotation_str,
- 'post_job_actions': {},
- 'workflow_outputs': []
- }
- # Connections
- input_connections = step.input_connections
- input_connections_type = {}
- multiple_input = {} # Boolean value indicating if this can be mutliple
- if step.type is None or step.type == 'tool':
- # Determine full (prefixed) names of valid input datasets
- data_input_names = {}
-
- def callback( input, value, prefixed_name, prefixed_label ):
- if isinstance( input, DataToolParameter ) or isinstance( input, DataCollectionToolParameter ):
- data_input_names[ prefixed_name ] = True
- multiple_input[ prefixed_name ] = input.multiple
- if isinstance( input, DataToolParameter ):
- input_connections_type[ input.name ] = "dataset"
- if isinstance( input, DataCollectionToolParameter ):
- input_connections_type[ input.name ] = "dataset_collection"
- visit_input_values( module.tool.inputs, module.state.inputs, callback )
- # Filter
- # FIXME: this removes connection without displaying a message currently!
- input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
- # post_job_actions
- pja_dict = {}
- for pja in step.post_job_actions:
- pja_dict[pja.action_type + pja.output_name] = dict(
- action_type=pja.action_type,
- output_name=pja.output_name,
- action_arguments=pja.action_arguments
- )
- step_dict['post_job_actions'] = pja_dict
- #workflow outputs
- outputs = []
- for output in step.workflow_outputs:
- outputs.append(output.output_name)
- step_dict['workflow_outputs'] = outputs
- # Encode input connections as dictionary
- input_conn_dict = {}
- for conn in input_connections:
- input_type = "dataset"
- if conn.input_name in input_connections_type:
- input_type = input_connections_type[ conn.input_name ]
- conn_dict = dict( id=conn.output_step.order_index, output_name=conn.output_name, input_type=input_type )
- if conn.input_name in multiple_input:
- if conn.input_name in input_conn_dict:
- input_conn_dict[ conn.input_name ].append( conn_dict )
- else:
- input_conn_dict[ conn.input_name ] = [ conn_dict ]
- else:
- input_conn_dict[ conn.input_name ] = conn_dict
- step_dict['input_connections'] = input_conn_dict
- # Position
- step_dict['position'] = step.position
- # Add to return value
- data['steps'][step.order_index] = step_dict
- return data
+ workflow_contents_manager = workflows.WorkflowContentsManager()
+ return workflow_contents_manager.workflow_to_dict( trans, stored, style="editor" )
@web.json
def save_workflow( self, trans, id, workflow_data ):
diff -r 2647ac82f98dcd90a133be765122f97d6dede45e -r b05becef6c64a4eb63bdc814a21243f2f5af799c test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -324,6 +324,46 @@
first_input = downloaded_workflow[ "steps" ][ "0" ][ "inputs" ][ 0 ]
assert first_input[ "name" ] == "WorkflowInput1"
assert first_input[ "description" ] == "input1 description"
+ self._assert_has_keys( downloaded_workflow, "a_galaxy_workflow", "format-version", "annotation", "uuid", "steps" )
+ for step in downloaded_workflow["steps"].values():
+ self._assert_has_keys(
+ step,
+ 'id',
+ 'type',
+ 'tool_id',
+ 'tool_version',
+ 'name',
+ 'tool_state',
+ 'tool_errors',
+ 'annotation',
+ 'inputs',
+ 'user_outputs',
+ 'outputs'
+ )
+ if step['type'] == "tool":
+ self._assert_has_keys( step, "post_job_actions" )
+
+ def test_export_editor( self ):
+ uploaded_workflow_id = self.workflow_populator.simple_workflow( "test_for_export" )
+ downloaded_workflow = self._download_workflow( uploaded_workflow_id, style="editor" )
+ self._assert_has_keys( downloaded_workflow, "name", "steps", "upgrade_messages" )
+ for step in downloaded_workflow["steps"].values():
+ self._assert_has_keys(
+ step,
+ 'id',
+ 'type',
+ 'tool_id',
+ 'name',
+ 'tool_state',
+ 'tooltip',
+ 'tool_errors',
+ 'data_inputs',
+ 'data_outputs',
+ 'form_html',
+ 'annotation',
+ 'post_job_actions',
+ 'workflow_outputs',
+ )
def test_import_export_with_runtime_inputs( self ):
workflow = self.workflow_populator.load_workflow_from_resource( name="test_workflow_with_runtime_input" )
@@ -771,8 +811,11 @@
)
return self._post( route, import_data )
- def _download_workflow(self, workflow_id):
- download_response = self._get( "workflows/%s/download" % workflow_id )
+ def _download_workflow(self, workflow_id, style=None):
+ params = {}
+ if style:
+ params = {"style": style}
+ download_response = self._get( "workflows/%s/download" % workflow_id, params )
self._assert_status_code_is( download_response, 200 )
downloaded_workflow = download_response.json()
return downloaded_workflow
https://bitbucket.org/galaxy/galaxy-central/commits/3e807234d175/
Changeset: 3e807234d175
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Introduce yet another workflow to_dict style - 'instance'.
This is the style used by workflow 'show' - this consolidates all step handling to_dict style-logic from controller/base.py, controller/workflow.py, and api/workflows.py into the new WorkflowContentsManager - with API test cases for everything.
Affected #: 2 files
diff -r b05becef6c64a4eb63bdc814a21243f2f5af799c -r 3e807234d17565ef13a6ade7157af87c690c630b lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -317,12 +317,17 @@
def workflow_to_dict( self, trans, stored, style="export" ):
""" Export the workflow contents to a dictionary ready for JSON-ification and to be
- sent out via API for instance. There are two styles of export allowed 'editor' and 'export'.
- The Galaxy team will do it best to preserve the backward compatibility of the 'export' stye -
- but the 'editor' style is subject to rapid and unannounced changes.
+ sent out via API for instance. There are three styles of export allowed 'export', 'instance', and
+ 'editor'. The Galaxy team will do it best to preserve the backward compatibility of the
+ 'export' stye - this is the export method meant to be portable across Galaxy instances and over
+ time. The 'editor' style is subject to rapid and unannounced changes. The 'instance' export
+ option describes the workflow in a context more tied to the current Galaxy instance and includes
+ fields like 'url' and 'url' and actual unencoded step ids instead of 'order_index'.
"""
if style == "editor":
return self._workflow_to_dict_editor( trans, stored )
+ elif style == "instance":
+ return self._workflow_to_dict_instance( trans, stored )
else:
return self._workflow_to_dict_export( trans, stored )
@@ -552,6 +557,45 @@
data['steps'][step.order_index] = step_dict
return data
+ def _workflow_to_dict_instance(self, trans, stored):
+ item = stored.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id } )
+ workflow = stored.latest_workflow
+ item['url'] = url_for('workflow', id=item['id'])
+ item['owner'] = stored.user.username
+ inputs = {}
+ for step in workflow.steps:
+ step_type = step.type
+ if step_type in ['data_input', 'data_collection_input']:
+ if step.tool_inputs and "name" in step.tool_inputs:
+ label = step.tool_inputs['name']
+ elif step_type == "data_input":
+ label = "Input Dataset"
+ elif step_type == "data_collection_input":
+ label = "Input Dataset Collection"
+ else:
+ raise ValueError("Invalid step_type %s" % step_type)
+ inputs[step.id] = {'label': label, 'value': ""}
+ else:
+ pass
+ # Eventually, allow regular tool parameters to be inserted and modified at runtime.
+ # p = step.get_required_parameters()
+ item['inputs'] = inputs
+ item['annotation'] = self.get_item_annotation_str( trans.sa_session, stored.user, stored )
+ steps = {}
+ for step in workflow.steps:
+ steps[step.id] = {'id': step.id,
+ 'type': step.type,
+ 'tool_id': step.tool_id,
+ 'tool_version': step.tool_version,
+ 'annotation': self.get_item_annotation_str( trans.sa_session, stored.user, step ),
+ 'tool_inputs': step.tool_inputs,
+ 'input_steps': {}}
+ for conn in step.input_connections:
+ steps[step.id]['input_steps'][conn.input_name] = {'source_step': conn.output_step_id,
+ 'step_output': conn.output_name}
+ item['steps'] = steps
+ return item
+
class MissingToolsException(object):
diff -r b05becef6c64a4eb63bdc814a21243f2f5af799c -r 3e807234d17565ef13a6ade7157af87c690c630b lib/galaxy/webapps/galaxy/api/workflows.py
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -76,8 +76,7 @@
if trans.sa_session.query(trans.app.model.StoredWorkflowUserShareAssociation).filter_by(user=trans.user, stored_workflow=stored_workflow).count() == 0:
message = "Workflow is neither importable, nor owned by or shared with current user"
raise exceptions.ItemAccessibilityException( message )
- latest_workflow = stored_workflow.latest_workflow
- return self.__encode_workflow( trans, stored_workflow, latest_workflow )
+ return self.workflow_contents_manager.workflow_to_dict( trans, stored_workflow, style="instance" )
@expose_api
def create(self, trans, payload, **kwd):
@@ -303,7 +302,7 @@
else:
message = "Updating workflow requires dictionary containing 'workflow' attribute with new JSON description."
raise exceptions.RequestParameterInvalidException( message )
- return self.__encode_workflow( trans, stored_workflow, workflow )
+ return self.workflow_contents_manager.workflow_to_dict( trans, stored_workflow, style="instance" )
def __api_import_new_workflow( self, trans, payload, **kwd ):
data = payload['workflow']
@@ -510,44 +509,6 @@
)
return self.__encode_invocation_step( trans, invocation_step )
- def __encode_workflow( self, trans, stored_workflow, workflow ):
- item = stored_workflow.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id } )
- item['url'] = url_for('workflow', id=item['id'])
- item['owner'] = stored_workflow.user.username
- inputs = {}
- for step in workflow.steps:
- step_type = step.type
- if step_type in ['data_input', 'data_collection_input']:
- if step.tool_inputs and "name" in step.tool_inputs:
- label = step.tool_inputs['name']
- elif step_type == "data_input":
- label = "Input Dataset"
- elif step_type == "data_collection_input":
- label = "Input Dataset Collection"
- else:
- raise ValueError("Invalid step_type %s" % step_type)
- inputs[step.id] = {'label': label, 'value': ""}
- else:
- pass
- # Eventually, allow regular tool parameters to be inserted and modified at runtime.
- # p = step.get_required_parameters()
- item['inputs'] = inputs
- item['annotation'] = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, stored_workflow )
- steps = {}
- for step in workflow.steps:
- steps[step.id] = {'id': step.id,
- 'type': step.type,
- 'tool_id': step.tool_id,
- 'tool_version': step.tool_version,
- 'annotation': self.get_item_annotation_str( trans.sa_session, stored_workflow.user, step ),
- 'tool_inputs': step.tool_inputs,
- 'input_steps': {}}
- for conn in step.input_connections:
- steps[step.id]['input_steps'][conn.input_name] = {'source_step': conn.output_step_id,
- 'step_output': conn.output_name}
- item['steps'] = steps
- return item
-
def __encode_invocation_step( self, trans, invocation_step ):
return self.encode_all_ids(
trans,
https://bitbucket.org/galaxy/galaxy-central/commits/4b542b64f6b2/
Changeset: 4b542b64f6b2
User: jmchilton
Date: 2014-12-16 03:20:11+00:00
Summary: Allow implicit connections between workflow steps (no editor GUI yet).
This means steps that are not connecting an output of one step to the input of another. This could potentially address all sorts of untraditional (in a Galaxy sense) workflows where some sort of data is managed externally. The most important use I think I have heard discussed is that of data managers - this can be used in cases where data managers depend on one another (grab the fasta files in one step, index them in another) or workflows where a downstream analysis depends on index data populated via data managers in earlier steps.
Not really sure how to represent these in the workflow editor - but this is a power user feature anyway so hopefully that is not super pressing. The YAML to workflow DSL supports the operation (see test cases) so these power users (a euphemism for Dan I guess) can just use that for now.
Affected #: 6 files
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -529,7 +529,7 @@
visit_input_values( module.tool.inputs, module.state.inputs, callback )
# Filter
# FIXME: this removes connection without displaying a message currently!
- input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
+ input_connections = [ conn for conn in input_connections if (conn.input_name in data_input_names or conn.non_data_connection) ]
# Encode input connections as dictionary
input_conn_dict = {}
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c lib/galaxy/model/__init__.py
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -3077,6 +3077,12 @@
class WorkflowStepConnection( object ):
+ # Constant used in lieu of output_name and input_name to indicate an
+ # implicit connection between two steps that is not dependent on a dataset
+ # or a dataset collection. Allowing for instance data manager steps to setup
+ # index data before a normal tool runs or for workflows that manage data
+ # outside of Galaxy.
+ NON_DATA_CONNECTION = "__NO_INPUT_OUTPUT_NAME__"
def __init__( self ):
self.output_step_id = None
@@ -3084,6 +3090,15 @@
self.input_step_id = None
self.input_name = None
+ def set_non_data_connection(self):
+ self.output_name = WorkflowStepConnection.NON_DATA_CONNECTION
+ self.input_name = WorkflowStepConnection.NON_DATA_CONNECTION
+
+ @property
+ def non_data_connection(self):
+ return (self.output_name == WorkflowStepConnection.NON_DATA_CONNECTION and
+ self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION)
+
class WorkflowOutput(object):
@@ -3153,6 +3168,13 @@
step_invocations[ step_id ].append( invocation_step )
return step_invocations
+ def step_invocations_for_step_id( self, step_id ):
+ step_invocations = []
+ for invocation_step in self.steps:
+ if step_id == invocation_step.workflow_step_id:
+ step_invocations.append( invocation_step )
+ return step_invocations
+
@staticmethod
def poll_active_workflow_ids(
sa_session,
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c lib/galaxy/workflow/run.py
--- a/lib/galaxy/workflow/run.py
+++ b/lib/galaxy/workflow/run.py
@@ -136,6 +136,8 @@
for step in remaining_steps:
jobs = None
try:
+ self.__check_implicitly_dependent_steps(step)
+
jobs = self._invoke_step( step )
for job in (util.listify( jobs ) or [None]):
# Record invocation
@@ -160,6 +162,38 @@
# invocations.
return self.progress.outputs
+ def __check_implicitly_dependent_steps( self, step ):
+ """ Method will delay the workflow evaluation if implicitly dependent
+ steps (steps dependent but not through an input->output way) are not
+ yet complete.
+ """
+ for input_connection in step.input_connections:
+ if input_connection.non_data_connection:
+ output_id = input_connection.output_step.id
+ self.__check_implicitly_dependent_step( output_id )
+
+ def __check_implicitly_dependent_step( self, output_id ):
+ step_invocations = self.workflow_invocation.step_invocations_for_step_id( output_id )
+
+ # No steps created yet - have to delay evaluation.
+ if not step_invocations:
+ raise modules.DelayedWorkflowEvaluation()
+
+ for step_invocation in step_invocations:
+ job = step_invocation.job
+ if job:
+ # At least one job in incomplete.
+ if not job.finished:
+ raise modules.DelayedWorkflowEvaluation()
+
+ if job.state != job.states.OK:
+ raise modules.CancelWorkflowEvaluation()
+
+ else:
+ # TODO: Handle implicit dependency on stuff like
+ # pause steps.
+ pass
+
def _invoke_step( self, step ):
jobs = step.module.execute( self.trans, self.progress, self.workflow_invocation, step )
return jobs
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -107,7 +107,7 @@
invocation_details = invocation_details_response.json()
return invocation_details
- def _run_jobs( self, jobs_yaml, history_id=None ):
+ def _run_jobs( self, jobs_yaml, history_id=None, wait=True ):
if history_id is None:
history_id = self.history_id
workflow_id = self._upload_yaml_workflow(
@@ -153,8 +153,9 @@
invocation_id = invocation[ "id" ]
# Wait for workflow to become fully scheduled and then for all jobs
# complete.
- self.wait_for_invocation( workflow_id, invocation_id )
- self.dataset_populator.wait_for_history( history_id, assert_ok=True )
+ if wait:
+ self.wait_for_invocation( workflow_id, invocation_id )
+ self.dataset_populator.wait_for_history( history_id, assert_ok=True )
jobs = self._history_jobs( history_id )
return RunJobsSummary(
history_id=history_id,
@@ -516,6 +517,60 @@
invocation = self._invocation_details( uploaded_workflow_id, invocation_id )
assert invocation[ 'state' ] == 'cancelled'
+ def test_run_with_implicit_connection( self ):
+ history_id = self.dataset_populator.new_history()
+ run_summary = self._run_jobs("""
+steps:
+- label: test_input
+ type: input
+- label: first_cat
+ tool_id: cat1
+ state:
+ input1:
+ $link: test_input
+- label: the_pause
+ type: pause
+ connect:
+ input:
+ - first_cat#out_file1
+- label: second_cat
+ tool_id: cat1
+ state:
+ input1:
+ $link: the_pause
+- label: third_cat
+ tool_id: random_lines1
+ connect:
+ $step: second_cat
+ state:
+ num_lines: 1
+ input:
+ $link: test_input
+ seed_source:
+ seed_source_selector: set_seed
+ seed: asdf
+ __current_case__: 1
+test_data:
+ test_input: "hello world"
+""", history_id=history_id, wait=False)
+ time.sleep( 2 )
+ history_id = run_summary.history_id
+ workflow_id = run_summary.workflow_id
+ invocation_id = run_summary.workflow_id
+ self.dataset_populator.wait_for_history( history_id, assert_ok=True )
+ invocation = self._invocation_details( workflow_id, invocation_id )
+ assert invocation[ 'state' ] != 'scheduled'
+ # Expect two jobs - the upload and first cat. randomlines shouldn't run
+ # it is implicitly dependent on second cat.
+ assert len( self._history_jobs( history_id ) ) == 2
+
+ self.__review_paused_steps( workflow_id, invocation_id, order_index=2, action=True )
+ self.wait_for_invocation( workflow_id, invocation_id )
+ time.sleep(1)
+ self.dataset_populator.wait_for_history( history_id, assert_ok=True )
+ time.sleep(1)
+ assert len( self._history_jobs( history_id ) ) == 4
+
def test_cannot_run_inaccessible_workflow( self ):
workflow = self.workflow_populator.load_workflow( name="test_for_run_cannot_access" )
workflow_request, history_id = self._setup_workflow_run( workflow )
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c test/api/test_workflows_from_yaml.py
--- a/test/api/test_workflows_from_yaml.py
+++ b/test/api/test_workflows_from_yaml.py
@@ -102,3 +102,34 @@
print self._get("workflows/%s/download" % workflow_id).json()
assert False
# TODO: fill out test...
+
+ def test_implicit_connections( self ):
+ workflow_id = self._upload_yaml_workflow("""
+- label: test_input
+ type: input
+- label: first_cat
+ tool_id: cat1
+ state:
+ input1:
+ $link: test_input
+- label: the_pause
+ type: pause
+ connect:
+ input:
+ - first_cat#out_file1
+- label: second_cat
+ tool_id: cat1
+ state:
+ input1:
+ $link: the_pause
+- label: third_cat
+ tool_id: cat1
+ connect:
+ $step: second_cat
+ state:
+ input1:
+ $link: test_input
+""")
+ workflow = self._get("workflows/%s/download" % workflow_id).json()
+ print workflow
+ assert False
diff -r 3e807234d17565ef13a6ade7157af87c690c630b -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c test/api/yaml_to_workflow.py
--- a/test/api/yaml_to_workflow.py
+++ b/test/api/yaml_to_workflow.py
@@ -262,6 +262,8 @@
values = [ values ]
for value in values:
if not isinstance(value, dict):
+ if key == "$step":
+ value += "#__NO_INPUT_OUTPUT_NAME__"
value_parts = str(value).split("#")
if len(value_parts) == 1:
value_parts.append("output")
@@ -270,6 +272,8 @@
id = context.labels[id]
value = {"id": int(id), "output_name": value_parts[1]}
input_connection_value.append(value)
+ if key == "$step":
+ key = "__NO_INPUT_OUTPUT_NAME__"
input_connections[key] = input_connection_value
https://bitbucket.org/galaxy/galaxy-central/commits/10d20731e6cc/
Changeset: 10d20731e6cc
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Slightly improved error message for incorrect workflow connection uploads.
Affected #: 1 file
diff -r 4b542b64f6b2add47fd55d535f06d026f26f7e3c -r 10d20731e6ccbe4a48a110d95ffe02a0e04ffcd3 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -201,6 +201,10 @@
if not isinstance(conn_list, list): # Older style singleton connection
conn_list = [conn_list]
for conn_dict in conn_list:
+ if 'output_name' not in conn_dict or 'id' not in conn_dict:
+ template = "Invalid connection [%s] - must be dict with output_name and id fields."
+ message = template % conn_dict
+ raise exceptions.MessageException(message)
conn = model.WorkflowStepConnection()
conn.input_step = step
conn.input_name = input_name
https://bitbucket.org/galaxy/galaxy-central/commits/afed68c51bc2/
Changeset: afed68c51bc2
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: De-duplication of step creation across update and create worklfows.
Affected #: 1 file
diff -r 10d20731e6ccbe4a48a110d95ffe02a0e04ffcd3 -r afed68c51bc23c80aa99c9379649476ffba4f796 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -167,14 +167,11 @@
# First pass to build step objects and populate basic values
for step_index in step_indices:
step_dict = supplied_steps[ step_index ]
- # Create the model class for the step
- step = model.WorkflowStep()
+
+ module, step = self.__module_from_dict( trans, step_dict, secure=False )
steps.append( step )
steps_by_external_id[ step_dict['id' ] ] = step
- # FIXME: Position should be handled inside module
- step.position = step_dict['position']
- module = module_factory.from_dict( trans, step_dict, secure=False )
- module.save_to_step( step )
+
if module.type == 'tool' and module.tool is None:
# A required tool is not available in the local Galaxy instance.
missing_tool_tup = ( step_dict[ 'tool_id' ], step_dict[ 'name' ], step_dict[ 'tool_version' ] )
@@ -185,13 +182,7 @@
step.config = json.dumps(step_dict)
if step.tool_errors:
workflow.has_errors = True
- # Stick this in the step temporarily
- step.temp_input_connections = step_dict['input_connections']
- # Save step annotation.
- annotation = step_dict[ 'annotation' ]
- if annotation:
- annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
+
# Second pass to deal with connections between steps
for step in steps:
# Input connections
@@ -269,14 +260,10 @@
# First pass to build step objects and populate basic values
for key, step_dict in data['steps'].iteritems():
+ module, step = self.__module_from_dict( trans, step_dict, secure=from_editor )
# Create the model class for the step
- step = model.WorkflowStep()
steps.append( step )
steps_by_external_id[ step_dict['id' ] ] = step
- # FIXME: Position should be handled inside module
- step.position = step_dict['position']
- module = module_factory.from_dict( trans, step_dict, secure=from_editor )
- module.save_to_step( step )
if 'workflow_outputs' in step_dict:
for output_name in step_dict['workflow_outputs']:
m = model.WorkflowOutput(workflow_step=step, output_name=output_name)
@@ -284,13 +271,7 @@
if step.tool_errors:
# DBTODO Check for conditional inputs here.
workflow.has_errors = True
- # Stick this in the step temporarily
- step.temp_input_connections = step_dict['input_connections']
- # Save step annotation.
- annotation = step_dict[ 'annotation' ]
- if annotation:
- annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
+
# Second pass to deal with connections between steps
for step in steps:
# Input connections
@@ -600,6 +581,28 @@
item['steps'] = steps
return item
+ def __module_from_dict( self, trans, step_dict, secure ):
+ """ Create a WorkflowStep model object and corrsponding module representing
+ type-specific functionality from the incoming dicitionary.
+ """
+ step = model.WorkflowStep()
+
+ # TODO: Consider handling position inside module.
+ step.position = step_dict['position']
+
+ module = module_factory.from_dict( trans, step_dict, secure=secure )
+ module.save_to_step( step )
+
+ annotation = step_dict[ 'annotation' ]
+ if annotation:
+ annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
+ self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
+
+ # Stick this in the step temporarily
+ step.temp_input_connections = step_dict['input_connections']
+
+ return module, step
+
class MissingToolsException(object):
https://bitbucket.org/galaxy/galaxy-central/commits/f7a77c6b57c7/
Changeset: f7a77c6b57c7
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: De-duplicatation for setting up workflow connections during creation/update.
Affected #: 1 file
diff -r afed68c51bc23c80aa99c9379649476ffba4f796 -r f7a77c6b57c74750097c3802185716ebd4e912b1 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -184,24 +184,7 @@
workflow.has_errors = True
# Second pass to deal with connections between steps
- for step in steps:
- # Input connections
- for input_name, conn_list in step.temp_input_connections.iteritems():
- if not conn_list:
- continue
- if not isinstance(conn_list, list): # Older style singleton connection
- conn_list = [conn_list]
- for conn_dict in conn_list:
- if 'output_name' not in conn_dict or 'id' not in conn_dict:
- template = "Invalid connection [%s] - must be dict with output_name and id fields."
- message = template % conn_dict
- raise exceptions.MessageException(message)
- conn = model.WorkflowStepConnection()
- conn.input_step = step
- conn.input_name = input_name
- conn.output_name = conn_dict['output_name']
- conn.output_step = steps_by_external_id[ conn_dict['id'] ]
- del step.temp_input_connections
+ self.__connect_workflow_steps( steps, steps_by_external_id )
# Order the steps if possible
attach_ordered_steps( workflow, steps )
@@ -273,18 +256,8 @@
workflow.has_errors = True
# Second pass to deal with connections between steps
- for step in steps:
- # Input connections
- for input_name, conns in step.temp_input_connections.iteritems():
- if conns:
- conn_dicts = conns if isinstance(conns, list) else [ conns ]
- for conn_dict in conn_dicts:
- conn = model.WorkflowStepConnection()
- conn.input_step = step
- conn.input_name = input_name
- conn.output_name = conn_dict['output_name']
- conn.output_step = steps_by_external_id[ conn_dict['id'] ]
- del step.temp_input_connections
+ self.__connect_workflow_steps( steps, steps_by_external_id )
+
# Order the steps if possible
attach_ordered_steps( workflow, steps )
# Connect up
@@ -603,6 +576,31 @@
return module, step
+ def __connect_workflow_steps( self, steps, steps_by_external_id ):
+ """ Second pass to deal with connections between steps.
+
+ Create workflow connection objects using externally specified ids
+ using during creation or update.
+ """
+ for step in steps:
+ # Input connections
+ for input_name, conn_list in step.temp_input_connections.iteritems():
+ if not conn_list:
+ continue
+ if not isinstance(conn_list, list): # Older style singleton connection
+ conn_list = [conn_list]
+ for conn_dict in conn_list:
+ if 'output_name' not in conn_dict or 'id' not in conn_dict:
+ template = "Invalid connection [%s] - must be dict with output_name and id fields."
+ message = template % conn_dict
+ raise exceptions.MessageException(message)
+ conn = model.WorkflowStepConnection()
+ conn.input_step = step
+ conn.input_name = input_name
+ conn.output_name = conn_dict['output_name']
+ conn.output_step = steps_by_external_id[ conn_dict['id'] ]
+ del step.temp_input_connections
+
class MissingToolsException(object):
https://bitbucket.org/galaxy/galaxy-central/commits/f9595a9840c1/
Changeset: f9595a9840c1
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Create abstraction for walking over step dicts to share between create/update methods.
Affected #: 1 file
diff -r f7a77c6b57c74750097c3802185716ebd4e912b1 -r f9595a9840c172a85fd8a5335688afb924afaa97 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -155,19 +155,7 @@
# the local Galaxy instance. Each tuple in the list of missing_tool_tups
# will be ( tool_id, tool_name, tool_version ).
missing_tool_tups = []
- supplied_steps = data[ 'steps' ]
- # Try to iterate through imported workflow in such a way as to
- # preserve step order.
- step_indices = supplied_steps.keys()
- try:
- step_indices = sorted( step_indices, key=int )
- except ValueError:
- # to defensive, were these ever or will they ever not be integers?
- pass
- # First pass to build step objects and populate basic values
- for step_index in step_indices:
- step_dict = supplied_steps[ step_index ]
-
+ for step_dict in self.__walk_step_dicts( data ):
module, step = self.__module_from_dict( trans, step_dict, secure=False )
steps.append( step )
steps_by_external_id[ step_dict['id' ] ] = step
@@ -242,7 +230,7 @@
raise MissingToolsException(workflow, errors)
# First pass to build step objects and populate basic values
- for key, step_dict in data['steps'].iteritems():
+ for step_dict in self.__walk_step_dicts( data ):
module, step = self.__module_from_dict( trans, step_dict, secure=from_editor )
# Create the model class for the step
steps.append( step )
@@ -554,6 +542,24 @@
item['steps'] = steps
return item
+ def __walk_step_dicts( self, data ):
+ """ Walk over the supplid step dictionaries and return them in a way designed
+ to preserve step order when possible.
+ """
+ supplied_steps = data[ 'steps' ]
+ # Try to iterate through imported workflow in such a way as to
+ # preserve step order.
+ step_indices = supplied_steps.keys()
+ try:
+ step_indices = sorted( step_indices, key=int )
+ except ValueError:
+ # to defensive, were these ever or will they ever not be integers?
+ pass
+ # First pass to build step objects and populate basic values
+ for step_index in step_indices:
+ step_dict = supplied_steps[ step_index ]
+ yield step_dict
+
def __module_from_dict( self, trans, step_dict, secure ):
""" Create a WorkflowStep model object and corrsponding module representing
type-specific functionality from the incoming dicitionary.
https://bitbucket.org/galaxy/galaxy-central/commits/10696df64a3a/
Changeset: 10696df64a3a
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Augment workflow step model for improved tracking.
Give every step a UUID that can be preserved across edit to the workflow. Likewise - allow every step to be given a label (a unique short name for that workflow) - that allows for a human consumable way to reference steps for use in tests and when driving workflows via the API. Workflow editor doesn't yet (and might never) display these attributes but it does preserve them across workflow saves.
Implement automated testing that uploading workflows, updating workflows, and exporting them handle UUID and labels. Manually tested workflow editor preserves labels and UUID across changes.
Affected #: 6 files
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 client/galaxy/scripts/galaxy.workflow_editor.canvas.js
--- a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js
+++ b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js
@@ -784,6 +784,8 @@
this.tooltip = data.tooltip ? data.tooltip : "";
this.annotation = data.annotation;
this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
+ this.label = data.label;
+ this.uuid = data.uuid;
this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
var node = this;
@@ -991,6 +993,8 @@
position : $(node.element).position(),
annotation: node.annotation,
post_job_actions: node.post_job_actions,
+ uuid: node.uuid,
+ label: node.label,
workflow_outputs: node.workflow_outputs
};
nodes[ node.id ] = node_data;
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -312,6 +312,8 @@
'annotation': annotation_str,
'input_connections': {},
'post_job_actions': {},
+ 'uuid': str(step.uuid),
+ 'label': step.label or None,
'workflow_outputs': []
}
# Position
@@ -344,6 +346,8 @@
'form_html': module.get_config_form(),
'annotation': annotation_str,
'post_job_actions': {},
+ 'uuid': str(step.uuid),
+ 'label': step.label or None,
'workflow_outputs': []
}
# Connections
@@ -438,6 +442,8 @@
'name': module.get_name(),
'tool_state': module.get_state( secure=False ),
'tool_errors': module.get_errors(),
+ 'uuid': str(step.uuid),
+ 'label': step.label or None,
## 'data_inputs': module.get_data_inputs(),
## 'data_outputs': module.get_data_outputs(),
'annotation': annotation_str
@@ -568,7 +574,10 @@
# TODO: Consider handling position inside module.
step.position = step_dict['position']
-
+ if "uuid" in step_dict:
+ step.uuid = step_dict["uuid"]
+ if "label" in step_dict:
+ step.label = step_dict["label"]
module = module_factory.from_dict( trans, step_dict, secure=secure )
module.save_to_step( step )
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 lib/galaxy/model/__init__.py
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -3074,6 +3074,7 @@
self.position = None
self.input_connections = []
self.config = None
+ self.uuid = uuid4()
class WorkflowStepConnection( object ):
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 lib/galaxy/model/mapping.py
--- a/lib/galaxy/model/mapping.py
+++ b/lib/galaxy/model/mapping.py
@@ -715,6 +715,8 @@
Column( "position", JSONType ),
Column( "config", JSONType ),
Column( "order_index", Integer ),
+ Column( "uuid", UUIDType ),
+ Column( "label", Unicode(255) ),
## Column( "input_connections", JSONType )
)
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 lib/galaxy/model/migrate/versions/0125_workflow_step_tracking.py
--- /dev/null
+++ b/lib/galaxy/model/migrate/versions/0125_workflow_step_tracking.py
@@ -0,0 +1,56 @@
+"""
+Migration script to enhance workflow step usability by adding labels and UUIDs.
+"""
+from sqlalchemy import *
+from sqlalchemy.orm import *
+from migrate import *
+from migrate.changeset import *
+from galaxy.model.custom_types import *
+
+import datetime
+now = datetime.datetime.utcnow
+
+import logging
+log = logging.getLogger( __name__ )
+
+metadata = MetaData()
+
+
+def upgrade(migrate_engine):
+ metadata.bind = migrate_engine
+ print __doc__
+ metadata.reflect()
+
+ StepLabel_column = Column( "label", TrimmedString(255) )
+ StepUUID_column = Column( "uuid", UUIDType, nullable=True )
+ __add_column( StepLabel_column, "workflow_step", metadata )
+ __add_column( StepUUID_column, "workflow_step", metadata )
+
+
+def downgrade(migrate_engine):
+ metadata.bind = migrate_engine
+ metadata.reflect()
+
+ for table in TABLES:
+ __drop(table)
+
+ __drop_column( "label", "workflow_step", metadata )
+ __drop_column( "uuid", "workflow_step", metadata )
+
+
+def __add_column(column, table_name, metadata, **kwds):
+ try:
+ table = Table( table_name, metadata, autoload=True )
+ column.create( table, **kwds )
+ except Exception as e:
+ print str(e)
+ log.exception( "Adding column %s column failed." % column)
+
+
+def __drop_column( column_name, table_name, metadata ):
+ try:
+ table = Table( table_name, metadata, autoload=True )
+ getattr( table.c, column_name ).drop()
+ except Exception as e:
+ print str(e)
+ log.exception( "Dropping column %s failed." % column_name )
diff -r f9595a9840c172a85fd8a5335688afb924afaa97 -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -3,6 +3,7 @@
from base import api
from json import dumps
from collections import namedtuple
+from uuid import uuid4
import time
@@ -227,9 +228,11 @@
def test_upload_deprecated( self ):
self.__test_upload( use_deprecated_route=True )
- def __test_upload( self, use_deprecated_route=False, name="test_import" ):
+ def __test_upload( self, use_deprecated_route=False, name="test_import", workflow=None ):
+ if workflow is None:
+ workflow = self.workflow_populator.load_workflow( name=name )
data = dict(
- workflow=dumps( self.workflow_populator.load_workflow( name=name ) ),
+ workflow=dumps( workflow ),
)
if use_deprecated_route:
route = "workflows/upload"
@@ -242,8 +245,25 @@
def test_update( self ):
original_workflow = self.workflow_populator.load_workflow( name="test_import" )
+ uuids = {}
+ labels = {}
- upload_response = self.__test_upload( )
+ for order_index, step_dict in original_workflow["steps"].iteritems():
+ uuid = str(uuid4())
+ step_dict["uuid"] = uuid
+ uuids[order_index] = uuid
+ label = "label_%s" % order_index
+ step_dict["label"] = label
+ labels[order_index] = label
+
+ def check_label_and_uuid(order_index, step_dict):
+ assert order_index in uuids
+ assert order_index in labels
+
+ self.assertEquals(uuids[order_index], step_dict["uuid"])
+ self.assertEquals(labels[order_index], step_dict["label"])
+
+ upload_response = self.__test_upload( workflow=original_workflow )
workflow_id = upload_response.json()["id"]
def update(workflow_object):
@@ -260,20 +280,24 @@
steps = workflow_content["steps"]
def tweak_step(step):
- assert step['position']['top'] != 1
- assert step['position']['left'] != 1
- step['position'] = {'top': 1, 'left': 1}
+ order_index, step_dict = step
+ check_label_and_uuid( order_index, step_dict)
+ assert step_dict['position']['top'] != 1
+ assert step_dict['position']['left'] != 1
+ step_dict['position'] = {'top': 1, 'left': 1}
- map(tweak_step, steps.values())
+ map(tweak_step, steps.iteritems())
update(workflow_content)
def check_step(step):
- assert step['position']['top'] == 1
- assert step['position']['left'] == 1
+ order_index, step_dict = step
+ check_label_and_uuid(order_index, step_dict)
+ assert step_dict['position']['top'] == 1
+ assert step_dict['position']['left'] == 1
updated_workflow_content = self._download_workflow(workflow_id)
- map(check_step, updated_workflow_content['steps'].values())
+ map(check_step, updated_workflow_content['steps'].iteritems())
# Re-update against original worklfow...
update(original_workflow)
@@ -281,7 +305,7 @@
updated_workflow_content = self._download_workflow(workflow_id)
# Make sure the positions have been updated.
- map(tweak_step, updated_workflow_content['steps'].values())
+ map(tweak_step, updated_workflow_content['steps'].iteritems())
def test_import_deprecated( self ):
workflow_id = self.workflow_populator.simple_workflow( "test_import_published_deprecated", publish=True )
@@ -364,6 +388,8 @@
'annotation',
'post_job_actions',
'workflow_outputs',
+ 'uuid',
+ 'label',
)
def test_import_export_with_runtime_inputs( self ):
https://bitbucket.org/galaxy/galaxy-central/commits/b6f28aec47bd/
Changeset: b6f28aec47bd
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Pack scripts (for workflow step tracking changes).
Affected #: 1 file
diff -r 10696df64a3a439ba87f0e3b6089ddd924c37f45 -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e static/scripts/galaxy.workflow_editor.canvas.js
--- a/static/scripts/galaxy.workflow_editor.canvas.js
+++ b/static/scripts/galaxy.workflow_editor.canvas.js
@@ -784,6 +784,8 @@
this.tooltip = data.tooltip ? data.tooltip : "";
this.annotation = data.annotation;
this.post_job_actions = data.post_job_actions ? data.post_job_actions : {};
+ this.label = data.label;
+ this.uuid = data.uuid;
this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
var node = this;
@@ -991,6 +993,8 @@
position : $(node.element).position(),
annotation: node.annotation,
post_job_actions: node.post_job_actions,
+ uuid: node.uuid,
+ label: node.label,
workflow_outputs: node.workflow_outputs
};
nodes[ node.id ] = node_data;
https://bitbucket.org/galaxy/galaxy-central/commits/2543786e0614/
Changeset: 2543786e0614
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Ensure unique step label and UUIDs across workflows during create/update.
Affected #: 5 files
diff -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e -r 2543786e06148d5ceabc603553828e4040882c7b lib/galaxy/exceptions/__init__.py
--- a/lib/galaxy/exceptions/__init__.py
+++ b/lib/galaxy/exceptions/__init__.py
@@ -51,6 +51,11 @@
err_code = error_codes.USER_SLUG_DUPLICATE
+class DuplicatedIdentifierException( MessageException ):
+ status_code = 400
+ err_code = error_codes.USER_IDENTIFIER_DUPLICATE
+
+
class ObjectAttributeInvalidException( MessageException ):
status_code = 400
err_code = error_codes.USER_OBJECT_ATTRIBUTE_INVALID
diff -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e -r 2543786e06148d5ceabc603553828e4040882c7b lib/galaxy/exceptions/error_codes.json
--- a/lib/galaxy/exceptions/error_codes.json
+++ b/lib/galaxy/exceptions/error_codes.json
@@ -55,6 +55,11 @@
"message": "The request contains unknown type of contents."
},
{
+ "name": "USER_IDENTIFIER_DUPLICATE",
+ "code": 400011,
+ "message": "Request contained a duplicated identifier that must be unique."
+ },
+ {
"name": "USER_TOOL_META_PARAMETER_PROBLEM",
"code": 400011,
"message": "Supplied incorrect or incompatible tool meta parameters."
diff -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e -r 2543786e06148d5ceabc603553828e4040882c7b lib/galaxy/managers/workflows.py
--- a/lib/galaxy/managers/workflows.py
+++ b/lib/galaxy/managers/workflows.py
@@ -561,9 +561,24 @@
except ValueError:
# to defensive, were these ever or will they ever not be integers?
pass
+
+ discovered_labels = set()
+ discovered_uuids = set()
+
# First pass to build step objects and populate basic values
for step_index in step_indices:
step_dict = supplied_steps[ step_index ]
+ uuid = step_dict.get("uuid", None)
+ if uuid:
+ if uuid in discovered_uuids:
+ raise exceptions.DuplicatedIdentifierException("Duplicate step UUID in request.")
+ discovered_uuids.add(uuid)
+ label = step_dict.get("label", None)
+ if label:
+ if label in discovered_labels:
+ raise exceptions.DuplicatedIdentifierException("Duplicated step label in request.")
+ discovered_labels.add(label)
+
yield step_dict
def __module_from_dict( self, trans, step_dict, secure ):
diff -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e -r 2543786e06148d5ceabc603553828e4040882c7b test/api/helpers.py
--- a/test/api/helpers.py
+++ b/test/api/helpers.py
@@ -198,13 +198,17 @@
return self.create_workflow( workflow, **create_kwds )
def create_workflow( self, workflow, **create_kwds ):
+ upload_response = self.create_workflow_response( workflow, **create_kwds )
+ uploaded_workflow_id = upload_response.json()[ "id" ]
+ return uploaded_workflow_id
+
+ def create_workflow_response( self, workflow, **create_kwds ):
data = dict(
workflow=json.dumps( workflow ),
**create_kwds
)
upload_response = self.galaxy_interactor.post( "workflows/upload", data=data )
- uploaded_workflow_id = upload_response.json()[ "id" ]
- return uploaded_workflow_id
+ return upload_response
class LibraryPopulator( object ):
diff -r b6f28aec47bd4fd6294a5aa5d1ea19fafcdb4f7e -r 2543786e06148d5ceabc603553828e4040882c7b test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -307,6 +307,21 @@
# Make sure the positions have been updated.
map(tweak_step, updated_workflow_content['steps'].iteritems())
+ def test_require_unique_step_uuids( self ):
+ workflow_dup_uuids = self.workflow_populator.load_workflow( name="test_import" )
+ uuid0 = str(uuid4())
+ for step_dict in workflow_dup_uuids["steps"].values():
+ step_dict["uuid"] = uuid0
+ response = self.workflow_populator.create_workflow_response( workflow_dup_uuids )
+ self._assert_status_code_is( response, 400 )
+
+ def test_require_unique_step_labels( self ):
+ workflow_dup_label = self.workflow_populator.load_workflow( name="test_import" )
+ for step_dict in workflow_dup_label["steps"].values():
+ step_dict["label"] = "my duplicated label"
+ response = self.workflow_populator.create_workflow_response( workflow_dup_label )
+ self._assert_status_code_is( response, 400 )
+
def test_import_deprecated( self ):
workflow_id = self.workflow_populator.simple_workflow( "test_import_published_deprecated", publish=True )
with self._different_user():
https://bitbucket.org/galaxy/galaxy-central/commits/52f58efee2dc/
Changeset: 52f58efee2dc
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Allow specifing workflow parameter replacements by step UUID.
Affected #: 3 files
diff -r 2543786e06148d5ceabc603553828e4040882c7b -r 52f58efee2dce225f5aeac7a382896eaad80baa8 lib/galaxy/workflow/run_request.py
--- a/lib/galaxy/workflow/run_request.py
+++ b/lib/galaxy/workflow/run_request.py
@@ -112,6 +112,10 @@
"""
param_dict = param_map.get(step.tool_id, {}).copy()
param_dict.update(param_map.get(str(step.id), {}))
+ step_uuid = step.uuid
+ if step_uuid:
+ uuid_params = param_map.get(str(step_uuid), {})
+ param_dict.update(uuid_params)
if param_dict:
if 'param' in param_dict and 'value' in param_dict:
param_dict[param_dict['param']] = param_dict['value']
diff -r 2543786e06148d5ceabc603553828e4040882c7b -r 52f58efee2dce225f5aeac7a382896eaad80baa8 test/api/test_workflow_2.ga
--- a/test/api/test_workflow_2.ga
+++ b/test/api/test_workflow_2.ga
@@ -25,6 +25,7 @@
"tool_state": "{\"name\": \"Input Dataset\"}",
"tool_version": null,
"type": "data_input",
+ "uuid": "58dffcc9-bcb7-4117-a0e1-61513524b3b0",
"user_outputs": []
},
"1": {
@@ -54,6 +55,7 @@
"tool_state": "{\"__page__\": 0, \"num_lines\": \"\\\"8\\\"\", \"seed_source\": \"{\\\"__current_case__\\\": 0, \\\"seed_source_selector\\\": \\\"no_seed\\\"}\", \"input\": \"null\", \"chromInfo\": \"\\\"/home/john/workspace/galaxy-central-workflows-params/tool-data/shared/ucsc/chrom/?.len\\\"\", \"__rerun_remap_job_id__\": null}",
"tool_version": null,
"type": "tool",
+ "uuid": "58dffcc9-bcb7-4117-a0e1-61513524b3b1",
"user_outputs": []
},
"2": {
@@ -83,6 +85,7 @@
"tool_state": "{\"__page__\": 0, \"num_lines\": \"\\\"6\\\"\", \"seed_source\": \"{\\\"__current_case__\\\": 0, \\\"seed_source_selector\\\": \\\"no_seed\\\"}\", \"input\": \"null\", \"chromInfo\": \"\\\"/home/john/workspace/galaxy-central-workflows-params/tool-data/shared/ucsc/chrom/?.len\\\"\", \"__rerun_remap_job_id__\": null}",
"tool_version": null,
"type": "tool",
+ "uuid": "58dffcc9-bcb7-4117-a0e1-61513524b3b2",
"user_outputs": []
}
}
diff -r 2543786e06148d5ceabc603553828e4040882c7b -r 52f58efee2dce225f5aeac7a382896eaad80baa8 test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -714,6 +714,20 @@
self.__assert_lines_hid_line_count_is( history_id, 2, 5 )
self.__assert_lines_hid_line_count_is( history_id, 3, 5 )
+ @skip_without_tool( "random_lines1" )
+ def test_run_replace_params_by_uuid( self ):
+ workflow_request, history_id = self._setup_random_x2_workflow( "test_for_replace_tool_params" )
+ workflow_request[ "parameters" ] = dumps( {
+ "58dffcc9-bcb7-4117-a0e1-61513524b3b1": dict( num_lines=4 ),
+ "58dffcc9-bcb7-4117-a0e1-61513524b3b2": dict( num_lines=3 ),
+ } )
+ run_workflow_response = self._post( "workflows", data=workflow_request )
+ self._assert_status_code_is( run_workflow_response, 200 )
+ self.dataset_populator.wait_for_history( history_id, assert_ok=True )
+ # Would be 8 and 6 without modification
+ self.__assert_lines_hid_line_count_is( history_id, 2, 4 )
+ self.__assert_lines_hid_line_count_is( history_id, 3, 3 )
+
@skip_without_tool( "validation_default" )
def test_parameter_substitution_validation( self ):
substitions = dict( input1="\" ; echo \"moo" )
https://bitbucket.org/galaxy/galaxy-central/commits/7ee34c597572/
Changeset: 7ee34c597572
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Allow specifing workflow inputs by step UUID.
Pass in the parameter 'inputs_by' as 'step_uuid' to the workflow run command to use this.
Specifing inputs by UUID has the nice advantage that it survives workflows saves - so if one sets up an API script or something to target a workflow - saving the workflow in the editor doesn't need to break the script as long as inputs were not added or deleted. The UUID (like the order_index) has the advantage of the step id that it is predeterminable - so one can set it up a workflow script against any Galaxy and the script doesn't need to be adapted to raw ids the steps get assigned in that instance.
Affected #: 2 files
diff -r 52f58efee2dce225f5aeac7a382896eaad80baa8 -r 7ee34c5975728c0f508a4283ef2f1ee35672ab0e lib/galaxy/workflow/run_request.py
--- a/lib/galaxy/workflow/run_request.py
+++ b/lib/galaxy/workflow/run_request.py
@@ -57,6 +57,8 @@
inputs_key = str( step.id )
elif inputs_by == "step_index":
inputs_key = str( step.order_index )
+ elif inputs_by == "step_uuid":
+ inputs_key = str( step.uuid )
elif inputs_by == "name":
inputs_key = step.tool_inputs.get( 'name', None )
else:
@@ -170,6 +172,11 @@
# of Galaxy at the time of workflow import.
inputs_by = inputs_by or 'step_index'
+ # TODO: with a little reworking this default could be
+ # step_index or step_uuid depending on what is inputs -
+ # unlike step_id and step_index there is not chance of
+ # conflicts.
+
add_to_history = 'no_add_to_history' not in payload
history_param = payload.get('history', '')
diff -r 52f58efee2dce225f5aeac7a382896eaad80baa8 -r 7ee34c5975728c0f508a4283ef2f1ee35672ab0e test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -78,6 +78,13 @@
elif inputs_by == "name":
workflow_request[ "inputs" ] = dumps( label_map )
workflow_request[ "inputs_by" ] = 'name'
+ elif inputs_by == "step_uuid":
+ uuid_map = {
+ workflow["steps"]["0"]["uuid"]: self._ds_entry(hda1),
+ workflow["steps"]["1"]["uuid"]: self._ds_entry(hda2),
+ }
+ workflow_request[ "inputs" ] = dumps( uuid_map )
+ workflow_request[ "inputs_by" ] = "step_uuid"
return workflow_request, history_id
@@ -421,6 +428,10 @@
self.__run_cat_workflow( inputs_by='step_index' )
@skip_without_tool( "cat1" )
+ def test_run_workflow_by_uuid( self ):
+ self.__run_cat_workflow( inputs_by='step_uuid' )
+
+ @skip_without_tool( "cat1" )
def test_run_workflow_by_name( self ):
self.__run_cat_workflow( inputs_by='name' )
@@ -430,11 +441,13 @@
def __run_cat_workflow( self, inputs_by ):
workflow = self.workflow_populator.load_workflow( name="test_for_run" )
+ workflow["steps"]["0"]["uuid"] = str(uuid4())
+ workflow["steps"]["1"]["uuid"] = str(uuid4())
workflow_request, history_id = self._setup_workflow_run( workflow, inputs_by=inputs_by )
# TODO: This should really be a post to workflows/<workflow_id>/run or
# something like that.
run_workflow_response = self._post( "workflows", data=workflow_request )
-
+ self._assert_status_code_is( run_workflow_response, 200 )
invocation_id = run_workflow_response.json()[ "id" ]
invocation = self._invocation_details( workflow_request[ "workflow_id" ], invocation_id )
assert invocation[ "state" ] == "scheduled", invocation
https://bitbucket.org/galaxy/galaxy-central/commits/8d5089f2ba81/
Changeset: 8d5089f2ba81
User: jmchilton
Date: 2014-12-16 03:20:12+00:00
Summary: Allow workflow inputs to be specifiable by UUID without explicit inputs_by.
Since there is no chance of conflict with step.id or step.order_index, just allow inputs or ds_map respectively to be indexed by UUID.
Affected #: 2 files
diff -r 7ee34c5975728c0f508a4283ef2f1ee35672ab0e -r 8d5089f2ba8183439a866026f3583c377f8bfa74 lib/galaxy/workflow/run_request.py
--- a/lib/galaxy/workflow/run_request.py
+++ b/lib/galaxy/workflow/run_request.py
@@ -53,18 +53,25 @@
if step.type not in INPUT_STEP_TYPES:
continue
- if inputs_by == "step_id":
- inputs_key = str( step.id )
- elif inputs_by == "step_index":
- inputs_key = str( step.order_index )
- elif inputs_by == "step_uuid":
- inputs_key = str( step.uuid )
- elif inputs_by == "name":
- inputs_key = step.tool_inputs.get( 'name', None )
- else:
- message = "Workflow cannot be run because unexpected inputs_by value specified."
- raise exceptions.MessageException( message )
- if inputs_key not in inputs:
+ possible_input_keys = []
+ for inputs_by_el in inputs_by.split("|"):
+ if inputs_by_el == "step_id":
+ possible_input_keys = [str( step.id )]
+ elif inputs_by_el == "step_index":
+ possible_input_keys = [str( step.order_index )]
+ elif inputs_by_el == "step_uuid":
+ possible_input_keys = [str( step.uuid )]
+ elif inputs_by_el == "name":
+ possible_input_keys = [step.tool_inputs.get( 'name', None )]
+ else:
+ message = "Workflow cannot be run because unexpected inputs_by value specified."
+ raise exceptions.MessageException( message )
+ inputs_key = None
+ for possible_input_key in possible_input_keys:
+ if possible_input_key in inputs:
+ inputs_key = possible_input_key
+
+ if not inputs_key:
message = "Workflow cannot be run because an expected input step '%s' has no input dataset." % step.id
raise exceptions.MessageException( message )
@@ -164,18 +171,13 @@
# Default to legacy behavior - read ds_map and reference steps
# by unencoded step id (a raw database id).
inputs = payload.get( 'ds_map', {} )
- inputs_by = inputs_by or 'step_id'
+ inputs_by = inputs_by or 'step_id|step_uuid'
else:
inputs = inputs or {}
# New default is to reference steps by index of workflow step
# which is intrinsic to the workflow and independent of the state
# of Galaxy at the time of workflow import.
- inputs_by = inputs_by or 'step_index'
-
- # TODO: with a little reworking this default could be
- # step_index or step_uuid depending on what is inputs -
- # unlike step_id and step_index there is not chance of
- # conflicts.
+ inputs_by = inputs_by or 'step_index|step_uuid'
add_to_history = 'no_add_to_history' not in payload
history_param = payload.get('history', '')
diff -r 7ee34c5975728c0f508a4283ef2f1ee35672ab0e -r 8d5089f2ba8183439a866026f3583c377f8bfa74 test/api/test_workflows.py
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -78,13 +78,14 @@
elif inputs_by == "name":
workflow_request[ "inputs" ] = dumps( label_map )
workflow_request[ "inputs_by" ] = 'name'
- elif inputs_by == "step_uuid":
+ elif inputs_by in [ "step_uuid", "uuid_implicitly" ]:
uuid_map = {
workflow["steps"]["0"]["uuid"]: self._ds_entry(hda1),
workflow["steps"]["1"]["uuid"]: self._ds_entry(hda2),
}
workflow_request[ "inputs" ] = dumps( uuid_map )
- workflow_request[ "inputs_by" ] = "step_uuid"
+ if inputs_by == "step_uuid":
+ workflow_request[ "inputs_by" ] = "step_uuid"
return workflow_request, history_id
@@ -432,6 +433,10 @@
self.__run_cat_workflow( inputs_by='step_uuid' )
@skip_without_tool( "cat1" )
+ def test_run_workflow_by_uuid_implicitly( self ):
+ self.__run_cat_workflow( inputs_by='uuid_implicitly' )
+
+ @skip_without_tool( "cat1" )
def test_run_workflow_by_name( self ):
self.__run_cat_workflow( inputs_by='name' )
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
0
2 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/45b206f6ab8f/
Changeset: 45b206f6ab8f
User: dannon
Date: 2014-12-16 00:38:00+00:00
Summary: Provide both user email and preferred contact email (if different) in error report form.
Affected #: 1 file
diff -r f06ac1a9f76efef1eb1cc174979184a73a01711d -r 45b206f6ab8fc8bf319766368d128f9b0fc6b4d1 lib/galaxy/tools/errors.py
--- a/lib/galaxy/tools/errors.py
+++ b/lib/galaxy/tools/errors.py
@@ -22,7 +22,7 @@
${history_view_link}
-----------------------------------------------------------------------------
-The user '${email}' provided the following information:
+The user ${email_str} provided the following information:
${message}
-----------------------------------------------------------------------------
@@ -74,12 +74,16 @@
else:
roles = []
return self.app.security_agent.can_access_dataset( roles, self.hda.dataset )
- def create_report( self, email='', message='', **kwd ):
+ def create_report( self, user, email='', message='', **kwd ):
hda = self.hda
job = self.job
host = web.url_for( '/', qualified=True )
history_view_link = web.url_for( controller="history", action="view", id=self.app.security.encode_id( hda.history_id ), qualified=True )
# Build the email message
+ if user and user.email != email:
+ email_str = "'%s' (providing preferred contact email '%s')" % (user.email, email)
+ else:
+ email_str = "'%s'" % email
self.report = string.Template( error_report_template ) \
.safe_substitute( host=host,
dataset_id=hda.dataset_id,
@@ -97,22 +101,22 @@
job_stdout=util.unicodify( job.stdout ),
job_info=util.unicodify( job.info ),
job_traceback=util.unicodify( job.traceback ),
- email=email,
+ email_str=email_str,
message=util.unicodify( message ) )
def _send_report( self, user, email=None, message=None, **kwd ):
return self.report
def send_report( self, user, email=None, message=None, **kwd ):
if self.report is None:
- self.create_report( email=email, message=message, **kwd )
+ self.create_report( user, email=email, message=message, **kwd )
return self._send_report( user, email=email, message=message, **kwd )
-
+
class EmailErrorReporter( ErrorReporter ):
def _send_report( self, user, email=None, message=None, **kwd ):
smtp_server = self.app.config.smtp_server
assert smtp_server, ValueError( "Mail is not configured for this galaxy instance" )
to_address = self.app.config.error_email_to
assert to_address, ValueError( "Error reporting has been disabled for this galaxy instance" )
-
+
frm = to_address
# Check email a bit
email = email or ''
https://bitbucket.org/galaxy/galaxy-central/commits/4f767bb3989f/
Changeset: 4f767bb3989f
User: dannon
Date: 2014-12-16 00:40:28+00:00
Summary: Pep8 standardized method spacing in lib/galaxy/tools/errors.py
Affected #: 1 file
diff -r 45b206f6ab8fc8bf319766368d128f9b0fc6b4d1 -r 4f767bb3989f2ee9b7e3fdb47fa4cdf716205fc6 lib/galaxy/tools/errors.py
--- a/lib/galaxy/tools/errors.py
+++ b/lib/galaxy/tools/errors.py
@@ -50,9 +50,10 @@
(This is an automated message).
"""
+
class ErrorReporter( object ):
def __init__( self, hda, app ):
- # Get the dataset
+ # Get the dataset
sa_session = app.model.context
if not isinstance( hda, model.HistoryDatasetAssociation ):
hda_id = hda
@@ -68,12 +69,14 @@
self.app = app
self.tool_id = self.job.tool_id
self.report = None
+
def _can_access_dataset( self, user ):
if user:
roles = user.all_roles()
else:
roles = []
return self.app.security_agent.can_access_dataset( roles, self.hda.dataset )
+
def create_report( self, user, email='', message='', **kwd ):
hda = self.hda
job = self.job
@@ -103,13 +106,16 @@
job_traceback=util.unicodify( job.traceback ),
email_str=email_str,
message=util.unicodify( message ) )
+
def _send_report( self, user, email=None, message=None, **kwd ):
return self.report
+
def send_report( self, user, email=None, message=None, **kwd ):
if self.report is None:
self.create_report( user, email=email, message=message, **kwd )
return self._send_report( user, email=email, message=message, **kwd )
+
class EmailErrorReporter( ErrorReporter ):
def _send_report( self, user, email=None, message=None, **kwd ):
smtp_server = self.app.config.smtp_server
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
0
2 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/3351ffb48668/
Changeset: 3351ffb48668
User: dannon
Date: 2014-12-15 19:01:47+00:00
Summary: Slightly more specific error message (and removal of the useless stack dump) for IOError when loading a tool.
Affected #: 1 file
diff -r 6d85572398e770efcd0fbf46cf9d7655795dd3e1 -r 3351ffb48668c9bb3076c2cfb6645e179b722535 lib/galaxy/tools/__init__.py
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -629,7 +629,9 @@
integrated_panel_dict[ key ] = tool
else:
integrated_panel_dict.insert( index, key, tool )
- except:
+ except IOError:
+ log.error( "Error reading tool configuration file from path: %s." % path )
+ except Exception:
log.exception( "Error reading tool from path: %s" % path )
def __add_tool( self, tool, load_panel_dict, panel_dict ):
https://bitbucket.org/galaxy/galaxy-central/commits/f06ac1a9f76e/
Changeset: f06ac1a9f76e
User: dannon
Date: 2014-12-15 22:26:32+00:00
Summary: Merge.
Affected #: 4 files
diff -r 3351ffb48668c9bb3076c2cfb6645e179b722535 -r f06ac1a9f76efef1eb1cc174979184a73a01711d templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
--- a/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
+++ b/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
@@ -57,7 +57,7 @@
<td>${tool_dependency.version|h}</td><td>${tool_dependency.type|h}</td><td>${tool_dependency.status|h}</td>
- <td>${error_message|h}</td>
+ <td>${error_message}</td></tr>
%endfor
</table>
diff -r 3351ffb48668c9bb3076c2cfb6645e179b722535 -r f06ac1a9f76efef1eb1cc174979184a73a01711d test/functional/tools/code_file.py
--- /dev/null
+++ b/test/functional/tools/code_file.py
@@ -0,0 +1,14 @@
+
+
+from galaxy.tools.parameters import DataToolParameter
+
+
+
+def validate_input( trans, error_map, param_values, page_param_map ):
+ """
+ Validates the user input, before execution.
+ """
+ first = param_values['name1']
+ second = param_values['name2']
+ if first == second:
+ error_map['name1'] = "The value names should be different."
diff -r 3351ffb48668c9bb3076c2cfb6645e179b722535 -r f06ac1a9f76efef1eb1cc174979184a73a01711d test/functional/tools/code_file.xml
--- /dev/null
+++ b/test/functional/tools/code_file.xml
@@ -0,0 +1,15 @@
+<tool id="code_file" name="code_file" version="1.0.0">
+ <description>code_file</description>
+ <code file="code_file.py" />
+ <command>cat $input1 > $output</command>
+ <inputs>
+ <param name="input1" type="data" label="Concatenate Dataset" />
+ <param name="name1" type="text" value="FactorName" label="value one should be different than value two" />
+ <param name="name2" type="text" value="FactorName" label="value one should be different than value two" />
+ </inputs>
+ <outputs>
+ <data format="txt" name="output" />
+ </outputs>
+ <tests>
+ </tests>
+</tool>
diff -r 3351ffb48668c9bb3076c2cfb6645e179b722535 -r f06ac1a9f76efef1eb1cc174979184a73a01711d test/functional/tools/samples_tool_conf.xml
--- a/test/functional/tools/samples_tool_conf.xml
+++ b/test/functional/tools/samples_tool_conf.xml
@@ -3,6 +3,7 @@
<tool file="upload.xml"/><tool file="simple_constructs.xml" /><tool file="composite.xml" />
+ <tool file="code_file.xml" /><tool file="disambiguate_cond.xml" /><tool file="multi_repeats.xml"/><tool file="multi_page.xml"/>
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
0
3 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/ae59198cc9b9/
Changeset: ae59198cc9b9
User: dan
Date: 2014-12-15 22:01:07+00:00
Summary: Do not |h escape tool dependency error message, as it is escaped and formated by tool_shed.util.basic_util.to_html_string
Affected #: 1 file
diff -r b89b8f8e57615b2a480d5abe13a111df159981e5 -r ae59198cc9b9ba4459881025f746bfda59fa98cd templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
--- a/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
+++ b/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
@@ -57,7 +57,7 @@
<td>${tool_dependency.version|h}</td><td>${tool_dependency.type|h}</td><td>${tool_dependency.status|h}</td>
- <td>${error_message|h}</td>
+ <td>${error_message}</td></tr>
%endfor
</table>
https://bitbucket.org/galaxy/galaxy-central/commits/d8b93cd3b491/
Changeset: d8b93cd3b491
Branch: next-stable
User: dan
Date: 2014-12-15 22:01:07+00:00
Summary: Do not |h escape tool dependency error message, as it is escaped and formated by tool_shed.util.basic_util.to_html_string
Affected #: 1 file
diff -r fc799502f7a6310f19230f58f11ab1fbe82b09ba -r d8b93cd3b491946703f52f74bf231c2aee339373 templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
--- a/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
+++ b/templates/admin/tool_shed_repository/manage_repository_tool_dependencies.mako
@@ -57,7 +57,7 @@
<td>${tool_dependency.version|h}</td><td>${tool_dependency.type|h}</td><td>${tool_dependency.status|h}</td>
- <td>${error_message|h}</td>
+ <td>${error_message}</td></tr>
%endfor
</table>
https://bitbucket.org/galaxy/galaxy-central/commits/c3d0d6202179/
Changeset: c3d0d6202179
User: dan
Date: 2014-12-15 22:02:44+00:00
Summary: merge
Affected #: 0 files
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
0
[galaxyproject/usegalaxy-playbook] e8119d: Banner for tomorrow's Corral downtime.
by GitHub 15 Dec '14
by GitHub 15 Dec '14
15 Dec '14
Branch: refs/heads/master
Home: https://github.com/galaxyproject/usegalaxy-playbook
Commit: e8119d8b39f7a631419a373386608b0e5f27cc4b
https://github.com/galaxyproject/usegalaxy-playbook/commit/e8119d8b39f7a631…
Author: Nate Coraor <nate(a)bx.psu.edu>
Date: 2014-12-15 (Mon, 15 Dec 2014)
Changed paths:
M files/galaxy/test.galaxyproject.org/var/integrated_tool_panel.xml
M production/group_vars/galaxyservers.yml
M stage/group_vars/galaxyservers.yml
Log Message:
-----------
Banner for tomorrow's Corral downtime.
1
0
commit/galaxy-central: jmchilton: Add code_file example tool from Bjoern.
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/b89b8f8e5761/
Changeset: b89b8f8e5761
User: jmchilton
Date: 2014-12-15 19:37:22+00:00
Summary: Add code_file example tool from Bjoern.
Affected #: 3 files
diff -r 6d85572398e770efcd0fbf46cf9d7655795dd3e1 -r b89b8f8e57615b2a480d5abe13a111df159981e5 test/functional/tools/code_file.py
--- /dev/null
+++ b/test/functional/tools/code_file.py
@@ -0,0 +1,14 @@
+
+
+from galaxy.tools.parameters import DataToolParameter
+
+
+
+def validate_input( trans, error_map, param_values, page_param_map ):
+ """
+ Validates the user input, before execution.
+ """
+ first = param_values['name1']
+ second = param_values['name2']
+ if first == second:
+ error_map['name1'] = "The value names should be different."
diff -r 6d85572398e770efcd0fbf46cf9d7655795dd3e1 -r b89b8f8e57615b2a480d5abe13a111df159981e5 test/functional/tools/code_file.xml
--- /dev/null
+++ b/test/functional/tools/code_file.xml
@@ -0,0 +1,15 @@
+<tool id="code_file" name="code_file" version="1.0.0">
+ <description>code_file</description>
+ <code file="code_file.py" />
+ <command>cat $input1 > $output</command>
+ <inputs>
+ <param name="input1" type="data" label="Concatenate Dataset" />
+ <param name="name1" type="text" value="FactorName" label="value one should be different than value two" />
+ <param name="name2" type="text" value="FactorName" label="value one should be different than value two" />
+ </inputs>
+ <outputs>
+ <data format="txt" name="output" />
+ </outputs>
+ <tests>
+ </tests>
+</tool>
diff -r 6d85572398e770efcd0fbf46cf9d7655795dd3e1 -r b89b8f8e57615b2a480d5abe13a111df159981e5 test/functional/tools/samples_tool_conf.xml
--- a/test/functional/tools/samples_tool_conf.xml
+++ b/test/functional/tools/samples_tool_conf.xml
@@ -3,6 +3,7 @@
<tool file="upload.xml"/><tool file="simple_constructs.xml" /><tool file="composite.xml" />
+ <tool file="code_file.xml" /><tool file="disambiguate_cond.xml" /><tool file="multi_repeats.xml"/><tool file="multi_page.xml"/>
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
0
[galaxyproject/usegalaxy-playbook] dee3d7: Update Test admin users and pull mutable configs.
by GitHub 15 Dec '14
by GitHub 15 Dec '14
15 Dec '14
Branch: refs/heads/master
Home: https://github.com/galaxyproject/usegalaxy-playbook
Commit: dee3d7903023d5acc86c937d1faadbf908bcb651
https://github.com/galaxyproject/usegalaxy-playbook/commit/dee3d7903023d5ac…
Author: Nate Coraor <nate(a)bx.psu.edu>
Date: 2014-12-15 (Mon, 15 Dec 2014)
Changed paths:
M files/galaxy/test.galaxyproject.org/var/integrated_tool_panel.xml
M files/galaxy/test.galaxyproject.org/var/shed_tool_conf.xml
M files/galaxy/test.galaxyproject.org/var/shed_tool_data_table_conf.xml
M stage/secret_vars/galaxyservers.yml
Log Message:
-----------
Update Test admin users and pull mutable configs.
1
0
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/6d85572398e7/
Changeset: 6d85572398e7
User: dannon
Date: 2014-12-15 16:18:20+00:00
Summary: Merge next-stable.
Affected #: 0 files
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
0
commit/galaxy-central: nsoranzo: Fix variable name causing a NameError.
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/fc799502f7a6/
Changeset: fc799502f7a6
Branch: next-stable
User: nsoranzo
Date: 2014-12-15 15:43:16+00:00
Summary: Fix variable name causing a NameError.
Affected #: 1 file
diff -r 6704c0ee9fec6b0624d739669cb7c66ea08d480a -r fc799502f7a6310f19230f58f11ab1fbe82b09ba lib/galaxy/managers/folders.py
--- a/lib/galaxy/managers/folders.py
+++ b/lib/galaxy/managers/folders.py
@@ -195,7 +195,7 @@
if ( ( len( encoded_folder_id ) % 16 == 1 ) and encoded_folder_id.startswith( 'F' ) ):
cut_id = encoded_folder_id[ 1: ]
else:
- raise exceptions.MalformedId( 'Malformed folder id ( %s ) specified, unable to decode.' % str( encoded_id ) )
+ raise exceptions.MalformedId( 'Malformed folder id ( %s ) specified, unable to decode.' % str( encoded_folder_id ) )
return cut_id
def decode_folder_id( self, trans, encoded_folder_id ):
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
0
commit/galaxy-central: nsoranzo: Fix variable name causing a NameError.
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/733696644b89/
Changeset: 733696644b89
User: nsoranzo
Date: 2014-12-15 15:43:16+00:00
Summary: Fix variable name causing a NameError.
Affected #: 1 file
diff -r 3c0acd32969bc282603607037522a2e16d70d074 -r 733696644b89ba39bbeeaf5908b97f4698d6868c lib/galaxy/managers/folders.py
--- a/lib/galaxy/managers/folders.py
+++ b/lib/galaxy/managers/folders.py
@@ -195,7 +195,7 @@
if ( ( len( encoded_folder_id ) % 16 == 1 ) and encoded_folder_id.startswith( 'F' ) ):
cut_id = encoded_folder_id[ 1: ]
else:
- raise exceptions.MalformedId( 'Malformed folder id ( %s ) specified, unable to decode.' % str( encoded_id ) )
+ raise exceptions.MalformedId( 'Malformed folder id ( %s ) specified, unable to decode.' % str( encoded_folder_id ) )
return cut_id
def decode_folder_id( self, trans, encoded_folder_id ):
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
0
[galaxyproject/usegalaxy-playbook] fdfbf4: added ga_code for google analytics purposes
by GitHub 15 Dec '14
by GitHub 15 Dec '14
15 Dec '14
Branch: refs/heads/master
Home: https://github.com/galaxyproject/usegalaxy-playbook
Commit: fdfbf4b48b6f86f9a524772c11a0f41a95fdad76
https://github.com/galaxyproject/usegalaxy-playbook/commit/fdfbf4b48b6f86f9…
Author: Marten <emulatorer(a)gmail.com>
Date: 2014-12-12 (Fri, 12 Dec 2014)
Changed paths:
M stage/group_vars/galaxyservers.yml
Log Message:
-----------
added ga_code for google analytics purposes
Commit: 9485f82f52b57445b6e45d63845bc162c7ab155b
https://github.com/galaxyproject/usegalaxy-playbook/commit/9485f82f52b57445…
Author: Marten <emulatorer(a)gmail.com>
Date: 2014-12-12 (Fri, 12 Dec 2014)
Changed paths:
M production/group_vars/galaxyservers.yml
Log Message:
-----------
add ga_code for google analytics purposes
Commit: 8f1785c81d7ee068b564f079dd02f4eaa33c49de
https://github.com/galaxyproject/usegalaxy-playbook/commit/8f1785c81d7ee068…
Author: Marten <emulatorer(a)gmail.com>
Date: 2014-12-12 (Fri, 12 Dec 2014)
Changed paths:
M templates/galaxy/common/static/welcome.html.j2
Log Message:
-----------
add ga script and onclick event sending
to track whether the cycle thing is used
Commit: 33e106b5e2f13eeb88b5c5356b4c49a87a615c62
https://github.com/galaxyproject/usegalaxy-playbook/commit/33e106b5e2f13eeb…
Author: Marten <emulatorer(a)gmail.com>
Date: 2014-12-12 (Fri, 12 Dec 2014)
Changed paths:
M templates/galaxy/common/static/welcome.html.j2
Log Message:
-----------
do not init ga twice
use the one in top.window instead
Commit: 1bbaae7a09fa230a97337d42d5b4cd754d377ba1
https://github.com/galaxyproject/usegalaxy-playbook/commit/1bbaae7a09fa230a…
Author: Marten <emulatorer(a)gmail.com>
Date: 2014-12-15 (Mon, 15 Dec 2014)
Changed paths:
M production/group_vars/galaxyservers.yml
M stage/group_vars/galaxyservers.yml
M templates/galaxy/common/static/welcome.html.j2
Log Message:
-----------
Merge pull request #2 from martenson/master
track how people click on the carousel links
Compare: https://github.com/galaxyproject/usegalaxy-playbook/compare/15b2fa183d58...…
1
0
commit/galaxy-central: dannon: Retab, whitespace in base.less
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/3c0acd32969b/
Changeset: 3c0acd32969b
User: dannon
Date: 2014-12-15 14:56:25+00:00
Summary: Retab, whitespace in base.less
Affected #: 1 file
diff -r 36457cc1ec26c440e7a996ce51eb87ed7c58cc4d -r 3c0acd32969bc282603607037522a2e16d70d074 static/style/src/less/base.less
--- a/static/style/src/less/base.less
+++ b/static/style/src/less/base.less
@@ -1,7 +1,7 @@
// bootstrap.less modified for Galaxy
@import "bootstrap.less";
-// Galaxy specific variables
+// Galaxy specific variables
@import "galaxy_variables.less";
@white: #fff;
@@ -92,23 +92,23 @@
}
#messagebox {
- position:absolute;
+ position:absolute;
top: @navbar-height + @layout_top_padding + 1;
- left:0;
- width:100%;
+ left:0;
+ width:100%;
height: @panel-message-height !important;
- overflow: hidden;
+ overflow: hidden;
border-bottom: solid #999 1px;
font-size: 90%;
}
#inactivebox {
- position:absolute;
+ position:absolute;
top: @navbar-height + @layout_top_padding + 1;
- left:0;
- width:100%;
+ left:0;
+ width:100%;
height: @panel-message-height !important;
- overflow: hidden;
+ overflow: hidden;
border-bottom: solid #999 1px;
font-size: 90%;
}
@@ -116,10 +116,10 @@
// Panels
#left, #left-border, #center, #right-border, #right {
- position: absolute;
+ position: absolute;
top: @navbar-height + @layout_top_padding + 1;
bottom: 0px;
- overflow: hidden;
+ overflow: hidden;
background: #fff;
}
#left, #center, #right, #left-border, #right-border {
@@ -145,7 +145,7 @@
}
#right {
width: 250px;
- right: 0px;
+ right: 0px;
z-index: 200;
border-left: solid @layout-border-color 1px;
}
@@ -277,11 +277,11 @@
}
}
}
-
+
.menu-bg {
background: whiteSmoke top repeat-x;
}
-
+
div.unified-panel-body {
position: absolute;
top: @panel_header_height;
@@ -326,10 +326,10 @@
z-index: 900; // Bootstrap elements start at 1000
position: absolute;
top: 0;
- left: 0;
+ left: 0;
width: 100%;
height: 100%;
-}
+}
// Messages for message box, slightly different style
@@ -345,19 +345,19 @@
background-position: 6px 50%;
}
-.panel-warning-message
+.panel-warning-message
{
background-image: url(warn_small.png);
background-color: @state-warning-bg;
}
-.panel-done-message
+.panel-done-message
{
background-image: url(ok_small.png);
background-color: @state-success-bg;
}
-.panel-info-message
+.panel-info-message
{
background-image: url(info_small.png);
background-color: @state-info-bg;
@@ -367,9 +367,9 @@
#masthead {
- position:absolute;
- top:0;
- left:0;
+ position:absolute;
+ top:0;
+ left:0;
width:100%;
min-width:990px;
padding: 0;
@@ -385,7 +385,7 @@
color: gold;
}
}
-
+
//.dropdown-menu {
// z-index: 15002;
//}
@@ -412,8 +412,8 @@
z-index: 2000;
img {
- display: inline;
- width: 26px;
+ display: inline;
+ width: 26px;
vertical-align: top;
}
a {
@@ -421,7 +421,7 @@
text-decoration: none;
}
}
-
+
.iconbar
{
position : absolute;
@@ -448,7 +448,7 @@
left : 23px;
top : -12px;
}
-
+
.toggle
{
color : gold;
@@ -542,7 +542,7 @@
border-color:#999;
}
-div.metadataHelpBody {
+div.metadataHelpBody {
width:100%;overflow:auto;
}
@@ -553,7 +553,7 @@
padding-bottom: 0.25em;
}
-// Forms
+// Forms
div.toolFormBody div.toolFormTitle {
background: transparent;
@@ -602,7 +602,7 @@
.border-radius(@panel-border-radius);
}
-div.form-title, div.toolFormTitle {
+div.form-title, div.toolFormTitle {
// font-size: @font-size-base * 1.25;
// line-height: @line-height-base * 1.5;
padding: 5px 10px;
@@ -610,7 +610,7 @@
border-bottom: solid @form-border 1px;
}
-div.form-body {
+div.form-body {
padding: 5px 0;
}
@@ -687,7 +687,7 @@
font: inherit;
}
-.form-row {
+.form-row {
select, textarea, input[type="text"], input[type="file"], input[type="password"] {
// -webkit-box-sizing: border-box;
max-width: 90%;
@@ -733,15 +733,15 @@
}
.search-spinner {
- position: absolute;
- display: none;
- right: 6px;
+ position: absolute;
+ display: none;
+ right: 6px;
top: 9px;
}
#search-clear-btn {
- position: absolute;
- right: 6px;
+ position: absolute;
+ right: 6px;
top: 9px;
display: block;
font-size: 1.4em !important;
@@ -753,7 +753,7 @@
}
}
-// Messages
+// Messages
.errormessagelarge, .warningmessagelarge, .donemessagelarge, .infomessagelarge {
.alert();
@@ -786,7 +786,7 @@
background-image: url(info_large.png);
}
-.screencastBox {
+.screencastBox {
padding-left: 10px;
border-color: #AAAA66;
background-color: #FFFFCC;
@@ -891,7 +891,7 @@
line-height: @line-height-base;
}
-table.colored tr.header {
+table.colored tr.header {
background: @table-heading-bg;
color: contrast(@complement-color-3);
background-repeat: repeat-x;
@@ -900,11 +900,11 @@
font-weight: bold;
}
-table.colored tr {
+table.colored tr {
background: @white;
}
-table.colored tr.odd_row {
+table.colored tr.odd_row {
background: @table-bg-accent;
}
@@ -1018,7 +1018,7 @@
height: 18px;
vertical-align: middle;
margin-left: 2px;
-
+
}
.search-box-input {
border: 0;
@@ -1051,7 +1051,7 @@
div.odd_row {
background: @table-bg-accent;
}
-
+
#footer {
display: none;
}
@@ -1199,15 +1199,15 @@
cursor: pointer;
position: relative;
.unselectable;
-
+
// border: solid transparent 1px;
// -moz-border-radius: 0.25em;
// -webkit-border-radius: 0.25em;
// border-radius: 0.25em;
-
+
// padding: 1px 0.25em;
// margin: -1px -0.25em;
-
+
a {
text-decoration: none;
}
@@ -1335,28 +1335,28 @@
background:#aaa;
color:#aaa;
}
-
+
table
{
border-collapse:collapse;
border-top:1px solid #ccc;
border-left:1px solid #ccc;
}
-
+
blockquote {
color:#666;
}
-
+
fieldset {
border-color:#ccc;
border:1px solid #ccc;
}
-
+
th,td {
border-bottom:1px solid #ddd;
border-right:1px solid #ccc;
}
-
+
th,td {
padding:.8em;
}
@@ -1438,7 +1438,7 @@
margin:10px 0;
padding-left:5px;
}
-
+
// ============================================================================ History
@import "sprite-history-states.less";
@import "sprite-history-buttons.less";
@@ -1457,7 +1457,7 @@
@import "ui/pagination.less";
-// ==== Tool menu styles
+// ==== Tool menu styles
.toolMenuContainer
{
@@ -1480,7 +1480,7 @@
}
div.toolSectionDetailsInner
-{
+{
margin-left: 5px;
margin-right: 5px;
}
@@ -1565,9 +1565,9 @@
.toolMenuAndView .toolForm
{
- float: left;
- background-color: white;
- margin: 10px;
+ float: left;
+ background-color: white;
+ margin: 10px;
}
// @import "base_sprites";
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
0
MiBuZXcgY29tbWl0cyBpbiBnYWxheHktY2VudHJhbDoKCmh0dHBzOi8vYml0YnVja2V0Lm9yZy9n
YWxheHkvZ2FsYXh5LWNlbnRyYWwvY29tbWl0cy84ZWM2OTA1MjYwMGEvCkNoYW5nZXNldDogICA4
ZWM2OTA1MjYwMGEKVXNlcjogICAgICAgIGRhbm5vbgpEYXRlOiAgICAgICAgMjAxNC0xMi0xNSAx
NDozOTo1NSswMDowMApTdW1tYXJ5OiAgICAgQWRkIGRlZmF1bHQgbnVtZXJpY2FsIGNvbHVtbiBo
ZWFkZXJzLCBwZXIgQmo9QzM9QjZybidzIFBSIChzbGk9CmdodGx5IHR3ZWFrZWQpIGh0dHBzOi8v
Yml0YnVja2V0Lm9yZy9nYWxheHkvZ2FsYXh5LWNlbnRyYWwvcHVsbC1yZXF1ZXN0LzYxMT0KL2Fk
ZC1hLWRlZmF1bHQtaGVhZGVyLXRvLXRhYnVsYXItb3V0cHV0Ly4KQWZmZWN0ZWQgIzogIDMgZmls
ZXMKCmRpZmYgLXIgN2U2OGFhM2Q4ZDE2MWZmMmEwNTI2MTAxYWVkZWJjMDI2YjRkNzFjYyAtciA4
ZWM2OTA1MjYwMGEyYzkxNWY4OGQzMT0KODU4ODRhMTg1ZTgyMDg2ZDEgY2xpZW50L2dhbGF4eS9z
Y3JpcHRzL212Yy9kYXRhLmpzCi0tLSBhL2NsaWVudC9nYWxheHkvc2NyaXB0cy9tdmMvZGF0YS5q
cworKysgYi9jbGllbnQvZ2FsYXh5L3NjcmlwdHMvbXZjL2RhdGEuanMKQEAgLTE3Niw2ICsxNzYs
MTAgQEAKICAgICAgICAgICAgIGhlYWRlcl9yb3cgPTNEICQoJzx0ci8+JykuY3NzKCdiYWNrZ3Jv
dW5kLWNvbG9yJywgdGhpcy5oZWFkZXJfPQpjb2xvcikuYXBwZW5kVG8oZGF0YV90YWJsZSk7CiAg
ICAgICAgIGlmIChjb2x1bW5fbmFtZXMpIHsKICAgICAgICAgICAgIGhlYWRlcl9yb3cuYXBwZW5k
KCc8dGg+JyArIGNvbHVtbl9uYW1lcy5qb2luKCc8L3RoPjx0aD4nKSArICc8PQovdGg+Jyk7Cisg
ICAgICAgIH0gZWxzZSB7CisgICAgICAgICAgICBmb3IgKHZhciBqID0zRCAxOyBqIDw9M0QgdGhp
cy5tb2RlbC5nZXRfbWV0YWRhdGEoJ2NvbHVtbnMnKTsgaj0KKyspIHsKKyAgICAgICAgICAgICAg
ICBoZWFkZXJfcm93LmFwcGVuZCgnPHRoPicgKyBqICsgJzwvdGg+Jyk7CisgICAgICAgICAgICB9
CiAgICAgICAgIH0KPTIwCiAgICAgICAgIC8vIFJlbmRlciBmaXJzdCBjaHVuay4KCmRpZmYgLXIg
N2U2OGFhM2Q4ZDE2MWZmMmEwNTI2MTAxYWVkZWJjMDI2YjRkNzFjYyAtciA4ZWM2OTA1MjYwMGEy
YzkxNWY4OGQzMT0KODU4ODRhMTg1ZTgyMDg2ZDEgc3RhdGljL3NjcmlwdHMvbXZjL2RhdGEuanMK
LS0tIGEvc3RhdGljL3NjcmlwdHMvbXZjL2RhdGEuanMKKysrIGIvc3RhdGljL3NjcmlwdHMvbXZj
L2RhdGEuanMKQEAgLTE3Niw2ICsxNzYsMTAgQEAKICAgICAgICAgICAgIGhlYWRlcl9yb3cgPTNE
ICQoJzx0ci8+JykuY3NzKCdiYWNrZ3JvdW5kLWNvbG9yJywgdGhpcy5oZWFkZXJfPQpjb2xvciku
YXBwZW5kVG8oZGF0YV90YWJsZSk7CiAgICAgICAgIGlmIChjb2x1bW5fbmFtZXMpIHsKICAgICAg
ICAgICAgIGhlYWRlcl9yb3cuYXBwZW5kKCc8dGg+JyArIGNvbHVtbl9uYW1lcy5qb2luKCc8L3Ro
Pjx0aD4nKSArICc8PQovdGg+Jyk7CisgICAgICAgIH0gZWxzZSB7CisgICAgICAgICAgICBmb3Ig
KHZhciBqID0zRCAxOyBqIDw9M0QgdGhpcy5tb2RlbC5nZXRfbWV0YWRhdGEoJ2NvbHVtbnMnKTsg
aj0KKyspIHsKKyAgICAgICAgICAgICAgICBoZWFkZXJfcm93LmFwcGVuZCgnPHRoPicgKyBqICsg
JzwvdGg+Jyk7CisgICAgICAgICAgICB9CiAgICAgICAgIH0KPTIwCiAgICAgICAgIC8vIFJlbmRl
ciBmaXJzdCBjaHVuay4KCmRpZmYgLXIgN2U2OGFhM2Q4ZDE2MWZmMmEwNTI2MTAxYWVkZWJjMDI2
YjRkNzFjYyAtciA4ZWM2OTA1MjYwMGEyYzkxNWY4OGQzMT0KODU4ODRhMTg1ZTgyMDg2ZDEgc3Rh
dGljL3NjcmlwdHMvcGFja2VkL212Yy9kYXRhLmpzCi0tLSBhL3N0YXRpYy9zY3JpcHRzL3BhY2tl
ZC9tdmMvZGF0YS5qcworKysgYi9zdGF0aWMvc2NyaXB0cy9wYWNrZWQvbXZjL2RhdGEuanMKQEAg
LTEsMSArMSwxIEBACi1kZWZpbmUoWyJtdmMvdWkvdWktbW9kYWwiLCJtdmMvdWkvdWktZnJhbWVz
IiwibXZjL3VpL2ljb24tYnV0dG9uIl0sZnVuY3Rpbz0KbihrLGosZil7dmFyIGg9M0RCYWNrYm9u
ZS5Nb2RlbC5leHRlbmQoe30pO3ZhciBiPTNEQmFja2JvbmUuTW9kZWwuZXh0ZW5kKHtkPQplZmF1
bHRzOntpZDoiIix0eXBlOiIiLG5hbWU6IiIsaGRhX2xkZGE6ImhkYSIsbWV0YWRhdGE6bnVsbH0s
aW5pdGlhbGl6ZTpmdW49CmN0aW9uKCl7aWYoIXRoaXMuZ2V0KCJtZXRhZGF0YSIpKXt0aGlzLl9z
ZXRfbWV0YWRhdGEoKX10aGlzLm9uKCJjaGFuZ2UiLHRoaT0Kcy5fc2V0X21ldGFkYXRhLHRoaXMp
fSxfc2V0X21ldGFkYXRhOmZ1bmN0aW9uKCl7dmFyIG49M0RuZXcgaCgpO18uZWFjaChfLmtlPQp5
cyh0aGlzLmF0dHJpYnV0ZXMpLGZ1bmN0aW9uKG8pe2lmKG8uaW5kZXhPZigibWV0YWRhdGFfIik9
M0Q9M0Q9M0QwKXt2YXIgcD0KPTNEby5zcGxpdCgibWV0YWRhdGFfIilbMV07bi5zZXQocCx0aGlz
LmF0dHJpYnV0ZXNbb10pO2RlbGV0ZSB0aGlzLmF0dHJpYnV0PQplc1tvXX19LHRoaXMpO3RoaXMu
c2V0KCJtZXRhZGF0YSIsbix7c2lsZW50OnRydWV9KX0sZ2V0X21ldGFkYXRhOmZ1bmN0aW9uKG49
Cil7cmV0dXJuIHRoaXMuYXR0cmlidXRlcy5tZXRhZGF0YS5nZXQobil9LHVybFJvb3Q6Z2FsYXh5
X2NvbmZpZy5yb290KyJhcGkvZD0KYXRhc2V0cyJ9KTt2YXIgaT0zRGIuZXh0ZW5kKHtkZWZhdWx0
czpfLmV4dGVuZCh7fSxiLnByb3RvdHlwZS5kZWZhdWx0cyx7Y2h1PQpua191cmw6bnVsbCxmaXJz
dF9kYXRhX2NodW5rOm51bGwsY2h1bmtfaW5kZXg6LTEsYXRfZW9mOmZhbHNlfSksaW5pdGlhbGl6
ZTo9CmZ1bmN0aW9uKG4pe2IucHJvdG90eXBlLmluaXRpYWxpemUuY2FsbCh0aGlzKTt0aGlzLmF0
dHJpYnV0ZXMuY2h1bmtfaW5kZXg9Cj0zRCh0aGlzLmF0dHJpYnV0ZXMuZmlyc3RfZGF0YV9jaHVu
az8xOjApO3RoaXMuYXR0cmlidXRlcy5jaHVua191cmw9M0RnYWxheD0KeV9jb25maWcucm9vdCsi
ZGF0YXNldC9kaXNwbGF5P2RhdGFzZXRfaWQ9M0QiK3RoaXMuaWQ7dGhpcy5hdHRyaWJ1dGVzLnVy
bF92PQppej0zRGdhbGF4eV9jb25maWcucm9vdCsidmlzdWFsaXphdGlvbiJ9LGdldF9uZXh0X2No
dW5rOmZ1bmN0aW9uKCl7aWYodGhpcy49CmF0dHJpYnV0ZXMuYXRfZW9mKXtyZXR1cm4gbnVsbH12
YXIgbj0zRHRoaXMsbz0zRCQuRGVmZXJyZWQoKTskLmdldEpTT04odGhpcz0KLmF0dHJpYnV0ZXMu
Y2h1bmtfdXJsLHtjaHVuazpuLmF0dHJpYnV0ZXMuY2h1bmtfaW5kZXgrK30pLnN1Y2Nlc3MoZnVu
Y3Rpb24oPQpwKXt2YXIgcTtpZihwLmNrX2RhdGEhPTNEPTNEIiIpe3E9M0RwfWVsc2V7bi5hdHRy
aWJ1dGVzLmF0X2VvZj0zRHRydWU7cT0zRG49CnVsbH1vLnJlc29sdmUocSl9KTtyZXR1cm4gb319
KTt2YXIgZT0zREJhY2tib25lLkNvbGxlY3Rpb24uZXh0ZW5kKHttb2RlbDpifT0KKTt2YXIgYT0z
REJhY2tib25lLlZpZXcuZXh0ZW5kKHtpbml0aWFsaXplOmZ1bmN0aW9uKG4pe3RoaXMucm93X2Nv
dW50PTNEMDt0PQpoaXMubG9hZGluZ19jaHVuaz0zRGZhbHNlO3RoaXMuaGVhZGVyX2NvbG9yPTNE
IiNBQUEiO3RoaXMuZGFya19yb3dfY29sb3I9M0Q9CiIjREREIjtuZXcgZCh7bW9kZWw6bi5tb2Rl
bCwkZWw6dGhpcy4kZWx9KX0sZXhwYW5kX3RvX2NvbnRhaW5lcjpmdW5jdGlvbigpez0KaWYodGhp
cy4kZWwuaGVpZ2h0KCk8dGhpcy5zY3JvbGxfZWx0LmhlaWdodCgpKXt0aGlzLmF0dGVtcHRfdG9f
ZmV0Y2goKX19LGF0PQp0ZW1wdF90b19mZXRjaDpmdW5jdGlvbihvKXt2YXIgbj0zRHRoaXM7aWYo
IXRoaXMubG9hZGluZ19jaHVuayYmdGhpcy5zY3JvbGw9CmVkX3RvX2JvdHRvbSgpKXt0aGlzLmxv
YWRpbmdfY2h1bms9M0R0cnVlO3RoaXMubG9hZGluZ19pbmRpY2F0b3Iuc2hvdygpOyQudz0KaGVu
KG4ubW9kZWwuZ2V0X25leHRfY2h1bmsoKSkudGhlbihmdW5jdGlvbihwKXtpZihwKXtuLl9yZW5k
ZXJDaHVuayhwKTtuLmxvPQphZGluZ19jaHVuaz0zRGZhbHNlfW4ubG9hZGluZ19pbmRpY2F0b3Iu
aGlkZSgpO24uZXhwYW5kX3RvX2NvbnRhaW5lcigpfSl9fSw9CnJlbmRlcjpmdW5jdGlvbigpe3Ro
aXMubG9hZGluZ19pbmRpY2F0b3I9M0QkKCI8ZGl2Lz4iKS5hdHRyKCJpZCIsImxvYWRpbmdfaT0K
bmRpY2F0b3IiKTt0aGlzLiRlbC5hcHBlbmQodGhpcy5sb2FkaW5nX2luZGljYXRvcik7dmFyIHE9
M0QkKCI8dGFibGUvPiIpLmF0PQp0cih7aWQ6ImNvbnRlbnRfdGFibGUiLGNlbGxwYWRkaW5nOjB9
KTt0aGlzLiRlbC5hcHBlbmQocSk7dmFyIG49M0R0aGlzLm1vZGU9CmwuZ2V0X21ldGFkYXRhKCJj
b2x1bW5fbmFtZXMiKSxyPTNEJCgiPHRyLz4iKS5jc3MoImJhY2tncm91bmQtY29sb3IiLHRoaXMu
aD0KZWFkZXJfY29sb3IpLmFwcGVuZFRvKHEpO2lmKG4pe3IuYXBwZW5kKCI8dGg+IituLmpvaW4o
IjwvdGg+PHRoPiIpKyI8L3RoPiIpPQp9dmFyIHA9M0R0aGlzLG89M0R0aGlzLm1vZGVsLmdldCgi
Zmlyc3RfZGF0YV9jaHVuayIpO2lmKG8pe3RoaXMuX3JlbmRlckNodW49Cmsobyl9ZWxzZXskLndo
ZW4ocC5tb2RlbC5nZXRfbmV4dF9jaHVuaygpKS50aGVuKGZ1bmN0aW9uKHMpe3AuX3JlbmRlckNo
dW5rKD0Kcyl9KX10aGlzLnNjcm9sbF9lbHQuc2Nyb2xsKGZ1bmN0aW9uKCl7cC5hdHRlbXB0X3Rv
X2ZldGNoKCl9KX0sc2Nyb2xsZWRfdG9fPQpib3R0b206ZnVuY3Rpb24oKXtyZXR1cm4gZmFsc2V9
LF9yZW5kZXJDZWxsOmZ1bmN0aW9uKHEsbixyKXt2YXIgbz0zRCQoIjx0ZD49CiIpLnRleHQocSk7
dmFyIHA9M0R0aGlzLm1vZGVsLmdldF9tZXRhZGF0YSgiY29sdW1uX3R5cGVzIik7aWYociE9M0Q9
M0R1bmRlZj0KaW5lZCl7by5hdHRyKCJjb2xzcGFuIixyKS5hZGRDbGFzcygic3RyaW5nYWxpZ24i
KX1lbHNle2lmKHApe2lmKG48cC5sZW5ndGgpPQp7aWYocFtuXT0zRD0zRD0zRCJzdHIifHxwW25d
PTNEPTNEPTNEImxpc3QiKXtvLmFkZENsYXNzKCJzdHJpbmdhbGlnbiIpfX19fXI9CmV0dXJuIG99
LF9yZW5kZXJSb3c6ZnVuY3Rpb24obil7dmFyIG89M0RuLnNwbGl0KCJcdCIpLHE9M0QkKCI8dHI+
IikscD0zRHRoaT0Kcy5tb2RlbC5nZXRfbWV0YWRhdGEoImNvbHVtbnMiKTtpZih0aGlzLnJvd19j
b3VudCUyIT0zRD0zRDApe3EuY3NzKCJiYWNrZ3JvPQp1bmQtY29sb3IiLHRoaXMuZGFya19yb3df
Y29sb3IpfWlmKG8ubGVuZ3RoPTNEPTNEPTNEcCl7Xy5lYWNoKG8sZnVuY3Rpb24ocyw9CnIpe3Eu
YXBwZW5kKHRoaXMuX3JlbmRlckNlbGwocyxyKSl9LHRoaXMpfWVsc2V7aWYoby5sZW5ndGg+cCl7
Xy5lYWNoKG8uc2xpYz0KZSgwLHAtMSksZnVuY3Rpb24ocyxyKXtxLmFwcGVuZCh0aGlzLl9yZW5k
ZXJDZWxsKHMscikpfSx0aGlzKTtxLmFwcGVuZCh0aGlzPQouX3JlbmRlckNlbGwoby5zbGljZShw
LTEpLmpvaW4oIlx0IikscC0xKSl9ZWxzZXtpZihwPjUmJm8ubGVuZ3RoPTNEPTNEPTNEcC09CjEp
e18uZWFjaChvLGZ1bmN0aW9uKHMscil7cS5hcHBlbmQodGhpcy5fcmVuZGVyQ2VsbChzLHIpKX0s
dGhpcyk7cS5hcHBlbmQoJD0KKCI8dGQ+IikpfWVsc2V7cS5hcHBlbmQodGhpcy5fcmVuZGVyQ2Vs
bChuLDAscCkpfX19dGhpcy5yb3dfY291bnQrKztyZXR1cm4gPQpxfSxfcmVuZGVyQ2h1bms6ZnVu
Y3Rpb24obil7dmFyIG89M0R0aGlzLiRlbC5maW5kKCJ0YWJsZSIpO18uZWFjaChuLmNrX2RhdGE9
Ci5zcGxpdCgiXG4iKSxmdW5jdGlvbihwLHEpe2lmKHAhPTNEPTNEIiIpe28uYXBwZW5kKHRoaXMu
X3JlbmRlclJvdyhwKSl9fSx0aD0KaXMpfX0pO3ZhciBnPTNEYS5leHRlbmQoe2luaXRpYWxpemU6
ZnVuY3Rpb24obil7YS5wcm90b3R5cGUuaW5pdGlhbGl6ZS5jYWxsPQoodGhpcyxuKTtzY3JvbGxf
ZWx0PTNEXy5maW5kKHRoaXMuJGVsLnBhcmVudHMoKSxmdW5jdGlvbihvKXtyZXR1cm4gJChvKS5j
c3M9Cigib3ZlcmZsb3ciKT0zRD0zRD0zRCJhdXRvIn0pO2lmKCFzY3JvbGxfZWx0KXtzY3JvbGxf
ZWx0PTNEd2luZG93fXRoaXMuc2Nybz0KbGxfZWx0PTNEJChzY3JvbGxfZWx0KX0sc2Nyb2xsZWRf
dG9fYm90dG9tOmZ1bmN0aW9uKCl7cmV0dXJuKHRoaXMuJGVsLmhlaWdoPQp0KCktdGhpcy5zY3Jv
bGxfZWx0LnNjcm9sbFRvcCgpLXRoaXMuc2Nyb2xsX2VsdC5oZWlnaHQoKTw9M0QwKX19KTt2YXIg
bT0zRGE9Ci5leHRlbmQoe2luaXRpYWxpemU6ZnVuY3Rpb24obil7YS5wcm90b3R5cGUuaW5pdGlh
bGl6ZS5jYWxsKHRoaXMsbik7dGhpcy5zYz0Kcm9sbF9lbHQ9M0R0aGlzLiRlbC5jc3Moe3Bvc2l0
aW9uOiJyZWxhdGl2ZSIsb3ZlcmZsb3c6InNjcm9sbCIsaGVpZ2h0OnRoaXMuPQpvcHRpb25zLmhl
aWdodHx8IjUwMHB4In0pfSxzY3JvbGxlZF90b19ib3R0b206ZnVuY3Rpb24oKXtyZXR1cm4gdGhp
cy4kZWwuc2M9CnJvbGxUb3AoKSt0aGlzLiRlbC5pbm5lckhlaWdodCgpPj0zRHRoaXMuZWwuc2Ny
b2xsSGVpZ2h0fX0pO3ZhciBkPTNEQmFja2Jvbj0KZS5WaWV3LmV4dGVuZCh7Y29sOntjaHJvbTpu
dWxsLHN0YXJ0Om51bGwsZW5kOm51bGx9LHVybF92aXo6bnVsbCxkYXRhc2V0X2lkPQo6bnVsbCxn
ZW5vbWVfYnVpbGQ6bnVsbCxmaWxlX2V4dDpudWxsLGluaXRpYWxpemU6ZnVuY3Rpb24ocCl7dmFy
IHM9M0RwYXJlbnQ9Ci5HYWxheHk7aWYocyYmcy5tb2RhbCl7dGhpcy5tb2RhbD0zRHMubW9kYWx9
aWYocyYmcy5mcmFtZSl7dGhpcy5mcmFtZT0zRHMuZj0KcmFtZX1pZighdGhpcy5tb2RhbHx8IXRo
aXMuZnJhbWUpe3JldHVybn12YXIgbz0zRHAubW9kZWw7dmFyIHI9M0RvLmdldCgibWV0PQphZGF0
YSIpO2lmKCFvLmdldCgiZmlsZV9leHQiKSl7cmV0dXJufXRoaXMuZmlsZV9leHQ9M0RvLmdldCgi
ZmlsZV9leHQiKTtpZig9CnRoaXMuZmlsZV9leHQ9M0Q9M0QiYmVkIil7aWYoci5nZXQoImNocm9t
Q29sIikmJnIuZ2V0KCJzdGFydENvbCIpJiZyLmdldCgiZT0KbmRDb2wiKSl7dGhpcy5jb2wuY2hy
b209M0RyLmdldCgiY2hyb21Db2wiKS0xO3RoaXMuY29sLnN0YXJ0PTNEci5nZXQoInN0YXJ0PQpD
b2wiKS0xO3RoaXMuY29sLmVuZD0zRHIuZ2V0KCJlbmRDb2wiKS0xfWVsc2V7Y29uc29sZS5sb2co
IlRhYnVsYXJCdXR0b25UcmE9CmNrc3RlciA6IEJlZC1maWxlIG1ldGFkYXRhIGluY29tcGxldGUu
Iik7cmV0dXJufX1pZih0aGlzLmZpbGVfZXh0PTNEPTNEInZjZj0KIil7ZnVuY3Rpb24gcSh1LHYp
e2Zvcih2YXIgdD0zRDA7dDx2Lmxlbmd0aDt0Kyspe2lmKHZbdF0ubWF0Y2godSkpe3JldHVybiB0
PQp9fXJldHVybiAtMX10aGlzLmNvbC5jaHJvbT0zRHEoIkNocm9tIixyLmdldCgiY29sdW1uX25h
bWVzIikpO3RoaXMuY29sLnN0YXI9CnQ9M0RxKCJQb3MiLHIuZ2V0KCJjb2x1bW5fbmFtZXMiKSk7
dGhpcy5jb2wuZW5kPTNEbnVsbDtpZih0aGlzLmNvbC5jaHJvbT0zRD0KPTNELTF8fHRoaXMuY29s
LnN0YXJ0PTNEPTNELTEpe2NvbnNvbGUubG9nKCJUYWJ1bGFyQnV0dG9uVHJhY2tzdGVyIDogVkNG
LWZpPQpsZSBtZXRhZGF0YSBpbmNvbXBsZXRlLiIpO3JldHVybn19aWYodGhpcy5jb2wuY2hyb209
M0Q9M0Q9M0R1bmRlZmluZWQpe3JldHU9CnJufWlmKG8uaWQpe3RoaXMuZGF0YXNldF9pZD0zRG8u
aWR9ZWxzZXtjb25zb2xlLmxvZygiVGFidWxhckJ1dHRvblRyYWNrc3Rlcj0KIDogRGF0YXNldCBp
ZGVudGlmaWNhdGlvbiBpcyBtaXNzaW5nLiIpO3JldHVybn1pZihvLmdldCgidXJsX3ZpeiIpKXt0
aGlzLnVyPQpsX3Zpej0zRG8uZ2V0KCJ1cmxfdml6Iil9ZWxzZXtjb25zb2xlLmxvZygiVGFidWxh
ckJ1dHRvblRyYWNrc3RlciA6IFVybCBmb3I9CiB2aXN1YWxpemF0aW9uIGNvbnRyb2xsZXIgaXMg
bWlzc2luZy4iKTtyZXR1cm59aWYoby5nZXQoImdlbm9tZV9idWlsZCIpKXt0aD0KaXMuZ2Vub21l
X2J1aWxkPTNEby5nZXQoImdlbm9tZV9idWlsZCIpfXZhciBuPTNEbmV3IGYuSWNvbkJ1dHRvblZp
ZXcoe21vZGVsPQo6bmV3IGYuSWNvbkJ1dHRvbih7dGl0bGU6IlZpc3VhbGl6ZSIsaWNvbl9jbGFz
czoiY2hhcnRfY3VydmUiLGlkOiJidG5fdml6In09Cil9KTt0aGlzLnNldEVsZW1lbnQocC4kZWwp
O3RoaXMuJGVsLmFwcGVuZChuLnJlbmRlcigpLiRlbCk7dGhpcy5oaWRlKCl9LGV2ZT0KbnRzOnsi
bW91c2VvdmVyIHRyIjoic2hvdyIsbW91c2VsZWF2ZToiaGlkZSJ9LHNob3c6ZnVuY3Rpb24ocyl7
ZnVuY3Rpb24gcih4PQope3JldHVybiAhaXNOYU4ocGFyc2VGbG9hdCh4KSkmJmlzRmluaXRlKHgp
fWlmKHRoaXMuY29sLmNocm9tPTNEPTNEPTNEbnVsbCk9CntyZXR1cm59dmFyIHc9M0QkKHMudGFy
Z2V0KS5wYXJlbnQoKTt2YXIgdD0zRHcuY2hpbGRyZW4oKS5lcSh0aGlzLmNvbC5jaHJvbT0KKS5o
dG1sKCk7dmFyIG49M0R3LmNoaWxkcmVuKCkuZXEodGhpcy5jb2wuc3RhcnQpLmh0bWwoKTt2YXIg
cD0zRHRoaXMuY29sLmVuPQpkP3cuY2hpbGRyZW4oKS5lcSh0aGlzLmNvbC5lbmQpLmh0bWwoKTpu
O2lmKCF0Lm1hdGNoKCJeIyIpJiZ0IT0zRD0zRCIiJiZyKG49Cikpe3ZhciB2PTNEe2RhdGFzZXRf
aWQ6dGhpcy5kYXRhc2V0X2lkLGdlbmVfcmVnaW9uOnQrIjoiK24rIi0iK3B9O3ZhciBxPTNEdz0K
Lm9mZnNldCgpO3ZhciBvPTNEcS5sZWZ0LTEwO3ZhciB1PTNEcS50b3AtJCh3aW5kb3cpLnNjcm9s
bFRvcCgpKzM7JCgiI2J0bl92PQppeiIpLmNzcyh7cG9zaXRpb246ImZpeGVkIix0b3A6dSsicHgi
LGxlZnQ6bysicHgifSk7JCgiI2J0bl92aXoiKS5vZmYoImNsaWM9CmsiKTskKCIjYnRuX3ZpeiIp
LmNsaWNrKHRoaXMuY3JlYXRlX3RyYWNrc3Rlcl9hY3Rpb24odGhpcy51cmxfdml6LHYsdGhpcy5n
ZT0Kbm9tZV9idWlsZCkpOyQoIiNidG5fdml6Iikuc2hvdygpfWVsc2V7JCgiI2J0bl92aXoiKS5o
aWRlKCl9fSxoaWRlOmZ1bmN0aW9uPQooKXt0aGlzLiRlbC5maW5kKCIjYnRuX3ZpeiIpLmhpZGUo
KX0sY3JlYXRlX3RyYWNrc3Rlcl9hY3Rpb246ZnVuY3Rpb24obixxLHA9Cil7dmFyIG89M0R0aGlz
O3JldHVybiBmdW5jdGlvbigpe3ZhciByPTNEe307aWYocCl7clsiZi1kYmtleSJdPTNEcH0kLmFq
YXgoez0KdXJsOm4rIi9saXN0X3RyYWNrcz8iKyQucGFyYW0ociksZGF0YVR5cGU6Imh0bWwiLGVy
cm9yOmZ1bmN0aW9uKCl7by5tb2RhbC5zPQpob3coe3RpdGxlOiJTb21ldGhpbmcgd2VudCB3cm9u
ZyEiLGJvZHk6IlVuZm9ydHVuYXRlbHkgd2UgY291bGQgbm90IGFkZCB0aGk9CnMgZGF0YXNldCB0
byB0aGUgdHJhY2sgYnJvd3Nlci4gUGxlYXNlIHRyeSBhZ2FpbiBvciBjb250YWN0IHVzLiIsYnV0
dG9uczp7Qz0KYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsLmhpZGUoKX19fSl9LHN1Y2Nlc3M6ZnVu
Y3Rpb24ocyl7by5tb2RhbC5zaG93KHt0aXRsPQplOiJWaWV3IERhdGEgaW4gYSBOZXcgb3IgU2F2
ZWQgVmlzdWFsaXphdGlvbiIsYnV0dG9uczp7Q2FuY2VsOmZ1bmN0aW9uKCl7by49Cm1vZGFsLmhp
ZGUoKX0sIlZpZXcgaW4gc2F2ZWQgdmlzdWFsaXphdGlvbiI6ZnVuY3Rpb24oKXtvLm1vZGFsLnNo
b3coe3RpdGxlOj0KIkFkZCBEYXRhIHRvIFNhdmVkIFZpc3VhbGl6YXRpb24iLGJvZHk6cyxidXR0
b25zOntDYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsPQouaGlkZSgpfSwiQWRkIHRvIHZpc3VhbGl6
YXRpb24iOmZ1bmN0aW9uKCl7by5tb2RhbC5oaWRlKCk7by5tb2RhbC4kZWwuZmluZCg9CiJpbnB1
dFtuYW1lPTNEaWRdOmNoZWNrZWQiKS5lYWNoKGZ1bmN0aW9uKCl7dmFyIHQ9M0QkKHRoaXMpLnZh
bCgpO3EuaWQ9M0R0Oz0Kby5mcmFtZS5hZGQoe3RpdGxlOiJUcmFja3N0ZXIiLHR5cGU6InVybCIs
Y29udGVudDpuKyIvdHJhY2tzdGVyPyIrJC5wYXJhbShxPQopfSl9KX19fSl9LCJWaWV3IGluIG5l
dyB2aXN1YWxpemF0aW9uIjpmdW5jdGlvbigpe28ubW9kYWwuaGlkZSgpO28uZnJhbWUuYWQ9CmQo
e3RpdGxlOiJUcmFja3N0ZXIiLHR5cGU6InVybCIsY29udGVudDpuKyIvdHJhY2tzdGVyPyIrJC5w
YXJhbShxKX0pfX19KX19KT0KO3JldHVybiBmYWxzZX19fSk7dmFyIGw9M0RmdW5jdGlvbihxLG8s
cixuKXt2YXIgcD0zRG5ldyBvKHttb2RlbDpuZXcgcShyKX0pPQo7cC5yZW5kZXIoKTtpZihuKXtu
LmFwcGVuZChwLiRlbCl9cmV0dXJuIHB9O3ZhciBjPTNEZnVuY3Rpb24ocCl7aWYoIXAubW9kZWw9
Cil7cC5tb2RlbD0zRG5ldyBpKHAuZGF0YXNldF9jb25maWcpfXZhciBvPTNEcC5wYXJlbnRfZWx0
O3ZhciBxPTNEcC5lbWJlZGRlZD0KO2RlbGV0ZSBwLmVtYmVkZGVkO2RlbGV0ZSBwLnBhcmVudF9l
bHQ7ZGVsZXRlIHAuZGF0YXNldF9jb25maWc7dmFyIG49M0QocT9uPQpldyBtKHApOm5ldyBnKHAp
KTtuLnJlbmRlcigpO2lmKG8pe28uYXBwZW5kKG4uJGVsKTtuLmV4cGFuZF90b19jb250YWluZXIo
KX09CnJldHVybiBufTtyZXR1cm57RGF0YXNldDpiLFRhYnVsYXJEYXRhc2V0OmksRGF0YXNldENv
bGxlY3Rpb246ZSxUYWJ1bGFyRGF0YT0Kc2V0Q2h1bmtlZFZpZXc6YSxjcmVhdGVUYWJ1bGFyRGF0
YXNldENodW5rZWRWaWV3OmN9fSk7ClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQorZGVmaW5l
KFsibXZjL3VpL3VpLW1vZGFsIiwibXZjL3VpL3VpLWZyYW1lcyIsIm12Yy91aS9pY29uLWJ1dHRv
biJdLGZ1bmN0aW89Cm4oayxqLGYpe3ZhciBoPTNEQmFja2JvbmUuTW9kZWwuZXh0ZW5kKHt9KTt2
YXIgYj0zREJhY2tib25lLk1vZGVsLmV4dGVuZCh7ZD0KZWZhdWx0czp7aWQ6IiIsdHlwZToiIixu
YW1lOiIiLGhkYV9sZGRhOiJoZGEiLG1ldGFkYXRhOm51bGx9LGluaXRpYWxpemU6ZnVuPQpjdGlv
bigpe2lmKCF0aGlzLmdldCgibWV0YWRhdGEiKSl7dGhpcy5fc2V0X21ldGFkYXRhKCl9dGhpcy5v
bigiY2hhbmdlIix0aGk9CnMuX3NldF9tZXRhZGF0YSx0aGlzKX0sX3NldF9tZXRhZGF0YTpmdW5j
dGlvbigpe3ZhciBuPTNEbmV3IGgoKTtfLmVhY2goXy5rZT0KeXModGhpcy5hdHRyaWJ1dGVzKSxm
dW5jdGlvbihvKXtpZihvLmluZGV4T2YoIm1ldGFkYXRhXyIpPTNEPTNEPTNEMCl7dmFyIHA9Cj0z
RG8uc3BsaXQoIm1ldGFkYXRhXyIpWzFdO24uc2V0KHAsdGhpcy5hdHRyaWJ1dGVzW29dKTtkZWxl
dGUgdGhpcy5hdHRyaWJ1dD0KZXNbb119fSx0aGlzKTt0aGlzLnNldCgibWV0YWRhdGEiLG4se3Np
bGVudDp0cnVlfSl9LGdldF9tZXRhZGF0YTpmdW5jdGlvbihuPQope3JldHVybiB0aGlzLmF0dHJp
YnV0ZXMubWV0YWRhdGEuZ2V0KG4pfSx1cmxSb290OmdhbGF4eV9jb25maWcucm9vdCsiYXBpL2Q9
CmF0YXNldHMifSk7dmFyIGk9M0RiLmV4dGVuZCh7ZGVmYXVsdHM6Xy5leHRlbmQoe30sYi5wcm90
b3R5cGUuZGVmYXVsdHMse2NodT0KbmtfdXJsOm51bGwsZmlyc3RfZGF0YV9jaHVuazpudWxsLGNo
dW5rX2luZGV4Oi0xLGF0X2VvZjpmYWxzZX0pLGluaXRpYWxpemU6PQpmdW5jdGlvbihuKXtiLnBy
b3RvdHlwZS5pbml0aWFsaXplLmNhbGwodGhpcyk7dGhpcy5hdHRyaWJ1dGVzLmNodW5rX2luZGV4
PQo9M0QodGhpcy5hdHRyaWJ1dGVzLmZpcnN0X2RhdGFfY2h1bms/MTowKTt0aGlzLmF0dHJpYnV0
ZXMuY2h1bmtfdXJsPTNEZ2FsYXg9CnlfY29uZmlnLnJvb3QrImRhdGFzZXQvZGlzcGxheT9kYXRh
c2V0X2lkPTNEIit0aGlzLmlkO3RoaXMuYXR0cmlidXRlcy51cmxfdj0KaXo9M0RnYWxheHlfY29u
ZmlnLnJvb3QrInZpc3VhbGl6YXRpb24ifSxnZXRfbmV4dF9jaHVuazpmdW5jdGlvbigpe2lmKHRo
aXMuPQphdHRyaWJ1dGVzLmF0X2VvZil7cmV0dXJuIG51bGx9dmFyIG49M0R0aGlzLG89M0QkLkRl
ZmVycmVkKCk7JC5nZXRKU09OKHRoaXM9Ci5hdHRyaWJ1dGVzLmNodW5rX3VybCx7Y2h1bms6bi5h
dHRyaWJ1dGVzLmNodW5rX2luZGV4Kyt9KS5zdWNjZXNzKGZ1bmN0aW9uKD0KcCl7dmFyIHE7aWYo
cC5ja19kYXRhIT0zRD0zRCIiKXtxPTNEcH1lbHNle24uYXR0cmlidXRlcy5hdF9lb2Y9M0R0cnVl
O3E9M0RuPQp1bGx9by5yZXNvbHZlKHEpfSk7cmV0dXJuIG99fSk7dmFyIGU9M0RCYWNrYm9uZS5D
b2xsZWN0aW9uLmV4dGVuZCh7bW9kZWw6Yn09Cik7dmFyIGE9M0RCYWNrYm9uZS5WaWV3LmV4dGVu
ZCh7aW5pdGlhbGl6ZTpmdW5jdGlvbihuKXt0aGlzLnJvd19jb3VudD0zRDA7dD0KaGlzLmxvYWRp
bmdfY2h1bms9M0RmYWxzZTt0aGlzLmhlYWRlcl9jb2xvcj0zRCIjQUFBIjt0aGlzLmRhcmtfcm93
X2NvbG9yPTNEPQoiI0RERCI7bmV3IGQoe21vZGVsOm4ubW9kZWwsJGVsOnRoaXMuJGVsfSl9LGV4
cGFuZF90b19jb250YWluZXI6ZnVuY3Rpb24oKXs9CmlmKHRoaXMuJGVsLmhlaWdodCgpPHRoaXMu
c2Nyb2xsX2VsdC5oZWlnaHQoKSl7dGhpcy5hdHRlbXB0X3RvX2ZldGNoKCl9fSxhdD0KdGVtcHRf
dG9fZmV0Y2g6ZnVuY3Rpb24obyl7dmFyIG49M0R0aGlzO2lmKCF0aGlzLmxvYWRpbmdfY2h1bmsm
JnRoaXMuc2Nyb2xsPQplZF90b19ib3R0b20oKSl7dGhpcy5sb2FkaW5nX2NodW5rPTNEdHJ1ZTt0
aGlzLmxvYWRpbmdfaW5kaWNhdG9yLnNob3coKTskLnc9CmhlbihuLm1vZGVsLmdldF9uZXh0X2No
dW5rKCkpLnRoZW4oZnVuY3Rpb24ocCl7aWYocCl7bi5fcmVuZGVyQ2h1bmsocCk7bi5sbz0KYWRp
bmdfY2h1bms9M0RmYWxzZX1uLmxvYWRpbmdfaW5kaWNhdG9yLmhpZGUoKTtuLmV4cGFuZF90b19j
b250YWluZXIoKX0pfX0sPQpyZW5kZXI6ZnVuY3Rpb24oKXt0aGlzLmxvYWRpbmdfaW5kaWNhdG9y
PTNEJCgiPGRpdi8+IikuYXR0cigiaWQiLCJsb2FkaW5nX2k9Cm5kaWNhdG9yIik7dGhpcy4kZWwu
YXBwZW5kKHRoaXMubG9hZGluZ19pbmRpY2F0b3IpO3ZhciByPTNEJCgiPHRhYmxlLz4iKS5hdD0K
dHIoe2lkOiJjb250ZW50X3RhYmxlIixjZWxscGFkZGluZzowfSk7dGhpcy4kZWwuYXBwZW5kKHIp
O3ZhciBuPTNEdGhpcy5tb2RlPQpsLmdldF9tZXRhZGF0YSgiY29sdW1uX25hbWVzIikscz0zRCQo
Ijx0ci8+IikuY3NzKCJiYWNrZ3JvdW5kLWNvbG9yIix0aGlzLmg9CmVhZGVyX2NvbG9yKS5hcHBl
bmRUbyhyKTtpZihuKXtzLmFwcGVuZCgiPHRoPiIrbi5qb2luKCI8L3RoPjx0aD4iKSsiPC90aD4i
KT0KfWVsc2V7Zm9yKHZhciBxPTNEMTtxPD0zRHRoaXMubW9kZWwuZ2V0X21ldGFkYXRhKCJjb2x1
bW5zIik7cSsrKXtzLmFwcGVuZCgiPQo8dGg+IitxKyI8L3RoPiIpfX12YXIgcD0zRHRoaXMsbz0z
RHRoaXMubW9kZWwuZ2V0KCJmaXJzdF9kYXRhX2NodW5rIik7aWYobyk9Cnt0aGlzLl9yZW5kZXJD
aHVuayhvKX1lbHNleyQud2hlbihwLm1vZGVsLmdldF9uZXh0X2NodW5rKCkpLnRoZW4oZnVuY3Rp
b24odD0KKXtwLl9yZW5kZXJDaHVuayh0KX0pfXRoaXMuc2Nyb2xsX2VsdC5zY3JvbGwoZnVuY3Rp
b24oKXtwLmF0dGVtcHRfdG9fZmV0Y2goPQopfSl9LHNjcm9sbGVkX3RvX2JvdHRvbTpmdW5jdGlv
bigpe3JldHVybiBmYWxzZX0sX3JlbmRlckNlbGw6ZnVuY3Rpb24ocSxuLHI9Cil7dmFyIG89M0Qk
KCI8dGQ+IikudGV4dChxKTt2YXIgcD0zRHRoaXMubW9kZWwuZ2V0X21ldGFkYXRhKCJjb2x1bW5f
dHlwZXMiKT0KO2lmKHIhPTNEPTNEdW5kZWZpbmVkKXtvLmF0dHIoImNvbHNwYW4iLHIpLmFkZENs
YXNzKCJzdHJpbmdhbGlnbiIpfWVsc2V7aWYoPQpwKXtpZihuPHAubGVuZ3RoKXtpZihwW25dPTNE
PTNEPTNEInN0ciJ8fHBbbl09M0Q9M0Q9M0QibGlzdCIpe28uYWRkQ2xhc3MoInM9CnRyaW5nYWxp
Z24iKX19fX1yZXR1cm4gb30sX3JlbmRlclJvdzpmdW5jdGlvbihuKXt2YXIgbz0zRG4uc3BsaXQo
Ilx0IikscT0zRD0KJCgiPHRyPiIpLHA9M0R0aGlzLm1vZGVsLmdldF9tZXRhZGF0YSgiY29sdW1u
cyIpO2lmKHRoaXMucm93X2NvdW50JTIhPTNEPTNEPQowKXtxLmNzcygiYmFja2dyb3VuZC1jb2xv
ciIsdGhpcy5kYXJrX3Jvd19jb2xvcil9aWYoby5sZW5ndGg9M0Q9M0Q9M0RwKXtfLmU9CmFjaChv
LGZ1bmN0aW9uKHMscil7cS5hcHBlbmQodGhpcy5fcmVuZGVyQ2VsbChzLHIpKX0sdGhpcyl9ZWxz
ZXtpZihvLmxlbmd0aD0KPnApe18uZWFjaChvLnNsaWNlKDAscC0xKSxmdW5jdGlvbihzLHIpe3Eu
YXBwZW5kKHRoaXMuX3JlbmRlckNlbGwocyxyKSl9LHRoPQppcyk7cS5hcHBlbmQodGhpcy5fcmVu
ZGVyQ2VsbChvLnNsaWNlKHAtMSkuam9pbigiXHQiKSxwLTEpKX1lbHNle2lmKHA+NSYmby49Cmxl
bmd0aD0zRD0zRD0zRHAtMSl7Xy5lYWNoKG8sZnVuY3Rpb24ocyxyKXtxLmFwcGVuZCh0aGlzLl9y
ZW5kZXJDZWxsKHMscikpfT0KLHRoaXMpO3EuYXBwZW5kKCQoIjx0ZD4iKSl9ZWxzZXtxLmFwcGVu
ZCh0aGlzLl9yZW5kZXJDZWxsKG4sMCxwKSl9fX10aGlzLnJvPQp3X2NvdW50Kys7cmV0dXJuIHF9
LF9yZW5kZXJDaHVuazpmdW5jdGlvbihuKXt2YXIgbz0zRHRoaXMuJGVsLmZpbmQoInRhYmxlIik9
CjtfLmVhY2gobi5ja19kYXRhLnNwbGl0KCJcbiIpLGZ1bmN0aW9uKHAscSl7aWYocCE9M0Q9M0Qi
Iil7by5hcHBlbmQodGhpcy5fcj0KZW5kZXJSb3cocCkpfX0sdGhpcyl9fSk7dmFyIGc9M0RhLmV4
dGVuZCh7aW5pdGlhbGl6ZTpmdW5jdGlvbihuKXthLnByb3RvdHlwPQplLmluaXRpYWxpemUuY2Fs
bCh0aGlzLG4pO3Njcm9sbF9lbHQ9M0RfLmZpbmQodGhpcy4kZWwucGFyZW50cygpLGZ1bmN0aW9u
KG89Cil7cmV0dXJuICQobykuY3NzKCJvdmVyZmxvdyIpPTNEPTNEPTNEImF1dG8ifSk7aWYoIXNj
cm9sbF9lbHQpe3Njcm9sbF9lbHQ9Cj0zRHdpbmRvd310aGlzLnNjcm9sbF9lbHQ9M0QkKHNjcm9s
bF9lbHQpfSxzY3JvbGxlZF90b19ib3R0b206ZnVuY3Rpb24oKXtyZT0KdHVybih0aGlzLiRlbC5o
ZWlnaHQoKS10aGlzLnNjcm9sbF9lbHQuc2Nyb2xsVG9wKCktdGhpcy5zY3JvbGxfZWx0LmhlaWdo
dCgpPQo8PTNEMCl9fSk7dmFyIG09M0RhLmV4dGVuZCh7aW5pdGlhbGl6ZTpmdW5jdGlvbihuKXth
LnByb3RvdHlwZS5pbml0aWFsaXplLmM9CmFsbCh0aGlzLG4pO3RoaXMuc2Nyb2xsX2VsdD0zRHRo
aXMuJGVsLmNzcyh7cG9zaXRpb246InJlbGF0aXZlIixvdmVyZmxvdzoicz0KY3JvbGwiLGhlaWdo
dDp0aGlzLm9wdGlvbnMuaGVpZ2h0fHwiNTAwcHgifSl9LHNjcm9sbGVkX3RvX2JvdHRvbTpmdW5j
dGlvbigpPQp7cmV0dXJuIHRoaXMuJGVsLnNjcm9sbFRvcCgpK3RoaXMuJGVsLmlubmVySGVpZ2h0
KCk+PTNEdGhpcy5lbC5zY3JvbGxIZWlnaHQ9Cn19KTt2YXIgZD0zREJhY2tib25lLlZpZXcuZXh0
ZW5kKHtjb2w6e2Nocm9tOm51bGwsc3RhcnQ6bnVsbCxlbmQ6bnVsbH0sdXJsXz0Kdml6Om51bGws
ZGF0YXNldF9pZDpudWxsLGdlbm9tZV9idWlsZDpudWxsLGZpbGVfZXh0Om51bGwsaW5pdGlhbGl6
ZTpmdW5jdGlvPQpuKHApe3ZhciBzPTNEcGFyZW50LkdhbGF4eTtpZihzJiZzLm1vZGFsKXt0aGlz
Lm1vZGFsPTNEcy5tb2RhbH1pZihzJiZzLmZyYW09CmUpe3RoaXMuZnJhbWU9M0RzLmZyYW1lfWlm
KCF0aGlzLm1vZGFsfHwhdGhpcy5mcmFtZSl7cmV0dXJufXZhciBvPTNEcC5tb2RlbD0KO3ZhciBy
PTNEby5nZXQoIm1ldGFkYXRhIik7aWYoIW8uZ2V0KCJmaWxlX2V4dCIpKXtyZXR1cm59dGhpcy5m
aWxlX2V4dD0zRG8uPQpnZXQoImZpbGVfZXh0Iik7aWYodGhpcy5maWxlX2V4dD0zRD0zRCJiZWQi
KXtpZihyLmdldCgiY2hyb21Db2wiKSYmci5nZXQoInM9CnRhcnRDb2wiKSYmci5nZXQoImVuZENv
bCIpKXt0aGlzLmNvbC5jaHJvbT0zRHIuZ2V0KCJjaHJvbUNvbCIpLTE7dGhpcy5jb2wucz0KdGFy
dD0zRHIuZ2V0KCJzdGFydENvbCIpLTE7dGhpcy5jb2wuZW5kPTNEci5nZXQoImVuZENvbCIpLTF9
ZWxzZXtjb25zb2xlLmxvPQpnKCJUYWJ1bGFyQnV0dG9uVHJhY2tzdGVyIDogQmVkLWZpbGUgbWV0
YWRhdGEgaW5jb21wbGV0ZS4iKTtyZXR1cm59fWlmKHRoaXM9Ci5maWxlX2V4dD0zRD0zRCJ2Y2Yi
KXtmdW5jdGlvbiBxKHUsdil7Zm9yKHZhciB0PTNEMDt0PHYubGVuZ3RoO3QrKyl7aWYodlt0XT0K
Lm1hdGNoKHUpKXtyZXR1cm4gdH19cmV0dXJuIC0xfXRoaXMuY29sLmNocm9tPTNEcSgiQ2hyb20i
LHIuZ2V0KCJjb2x1bW5fbmFtPQplcyIpKTt0aGlzLmNvbC5zdGFydD0zRHEoIlBvcyIsci5nZXQo
ImNvbHVtbl9uYW1lcyIpKTt0aGlzLmNvbC5lbmQ9M0RudWxsO2k9CmYodGhpcy5jb2wuY2hyb209
M0Q9M0QtMXx8dGhpcy5jb2wuc3RhcnQ9M0Q9M0QtMSl7Y29uc29sZS5sb2coIlRhYnVsYXJCdXR0
bz0KblRyYWNrc3RlciA6IFZDRi1maWxlIG1ldGFkYXRhIGluY29tcGxldGUuIik7cmV0dXJufX1p
Zih0aGlzLmNvbC5jaHJvbT0zRD0KPTNEPTNEdW5kZWZpbmVkKXtyZXR1cm59aWYoby5pZCl7dGhp
cy5kYXRhc2V0X2lkPTNEby5pZH1lbHNle2NvbnNvbGUubG9nKCJUPQphYnVsYXJCdXR0b25UcmFj
a3N0ZXIgOiBEYXRhc2V0IGlkZW50aWZpY2F0aW9uIGlzIG1pc3NpbmcuIik7cmV0dXJufWlmKG8u
Z2U9CnQoInVybF92aXoiKSl7dGhpcy51cmxfdml6PTNEby5nZXQoInVybF92aXoiKX1lbHNle2Nv
bnNvbGUubG9nKCJUYWJ1bGFyQnV0dD0Kb25UcmFja3N0ZXIgOiBVcmwgZm9yIHZpc3VhbGl6YXRp
b24gY29udHJvbGxlciBpcyBtaXNzaW5nLiIpO3JldHVybn1pZihvLmdlPQp0KCJnZW5vbWVfYnVp
bGQiKSl7dGhpcy5nZW5vbWVfYnVpbGQ9M0RvLmdldCgiZ2Vub21lX2J1aWxkIil9dmFyIG49M0Ru
ZXcgZi49Ckljb25CdXR0b25WaWV3KHttb2RlbDpuZXcgZi5JY29uQnV0dG9uKHt0aXRsZToiVmlz
dWFsaXplIixpY29uX2NsYXNzOiJjaGFydD0KX2N1cnZlIixpZDoiYnRuX3ZpeiJ9KX0pO3RoaXMu
c2V0RWxlbWVudChwLiRlbCk7dGhpcy4kZWwuYXBwZW5kKG4ucmVuZGVyKCkuPQokZWwpO3RoaXMu
aGlkZSgpfSxldmVudHM6eyJtb3VzZW92ZXIgdHIiOiJzaG93Iixtb3VzZWxlYXZlOiJoaWRlIn0s
c2hvdzpmdW49CmN0aW9uKHMpe2Z1bmN0aW9uIHIoeCl7cmV0dXJuICFpc05hTihwYXJzZUZsb2F0
KHgpKSYmaXNGaW5pdGUoeCl9aWYodGhpcy5jbz0KbC5jaHJvbT0zRD0zRD0zRG51bGwpe3JldHVy
bn12YXIgdz0zRCQocy50YXJnZXQpLnBhcmVudCgpO3ZhciB0PTNEdy5jaGlsZHJlPQpuKCkuZXEo
dGhpcy5jb2wuY2hyb20pLmh0bWwoKTt2YXIgbj0zRHcuY2hpbGRyZW4oKS5lcSh0aGlzLmNvbC5z
dGFydCkuaHRtbCg9Cik7dmFyIHA9M0R0aGlzLmNvbC5lbmQ/dy5jaGlsZHJlbigpLmVxKHRoaXMu
Y29sLmVuZCkuaHRtbCgpOm47aWYoIXQubWF0Y2goIj0KXiMiKSYmdCE9M0Q9M0QiIiYmcihuKSl7
dmFyIHY9M0R7ZGF0YXNldF9pZDp0aGlzLmRhdGFzZXRfaWQsZ2VuZV9yZWdpb246dCsiPQo6Iitu
KyItIitwfTt2YXIgcT0zRHcub2Zmc2V0KCk7dmFyIG89M0RxLmxlZnQtMTA7dmFyIHU9M0RxLnRv
cC0kKHdpbmRvdykuc2M9CnJvbGxUb3AoKSszOyQoIiNidG5fdml6IikuY3NzKHtwb3NpdGlvbjoi
Zml4ZWQiLHRvcDp1KyJweCIsbGVmdDpvKyJweCJ9KTskKD0KIiNidG5fdml6Iikub2ZmKCJjbGlj
ayIpOyQoIiNidG5fdml6IikuY2xpY2sodGhpcy5jcmVhdGVfdHJhY2tzdGVyX2FjdGlvbih0PQpo
aXMudXJsX3Zpeix2LHRoaXMuZ2Vub21lX2J1aWxkKSk7JCgiI2J0bl92aXoiKS5zaG93KCl9ZWxz
ZXskKCIjYnRuX3ZpeiIpLmg9CmlkZSgpfX0saGlkZTpmdW5jdGlvbigpe3RoaXMuJGVsLmZpbmQo
IiNidG5fdml6IikuaGlkZSgpfSxjcmVhdGVfdHJhY2tzdGVyXz0KYWN0aW9uOmZ1bmN0aW9uKG4s
cSxwKXt2YXIgbz0zRHRoaXM7cmV0dXJuIGZ1bmN0aW9uKCl7dmFyIHI9M0R7fTtpZihwKXtyWyJm
PQotZGJrZXkiXT0zRHB9JC5hamF4KHt1cmw6bisiL2xpc3RfdHJhY2tzPyIrJC5wYXJhbShyKSxk
YXRhVHlwZToiaHRtbCIsZXJyb3I9CjpmdW5jdGlvbigpe28ubW9kYWwuc2hvdyh7dGl0bGU6IlNv
bWV0aGluZyB3ZW50IHdyb25nISIsYm9keToiVW5mb3J0dW5hdGVseT0KIHdlIGNvdWxkIG5vdCBh
ZGQgdGhpcyBkYXRhc2V0IHRvIHRoZSB0cmFjayBicm93c2VyLiBQbGVhc2UgdHJ5IGFnYWluIG9y
IGNvPQpudGFjdCB1cy4iLGJ1dHRvbnM6e0NhbmNlbDpmdW5jdGlvbigpe28ubW9kYWwuaGlkZSgp
fX19KX0sc3VjY2VzczpmdW5jdGlvbig9CnMpe28ubW9kYWwuc2hvdyh7dGl0bGU6IlZpZXcgRGF0
YSBpbiBhIE5ldyBvciBTYXZlZCBWaXN1YWxpemF0aW9uIixidXR0b25zOj0Ke0NhbmNlbDpmdW5j
dGlvbigpe28ubW9kYWwuaGlkZSgpfSwiVmlldyBpbiBzYXZlZCB2aXN1YWxpemF0aW9uIjpmdW5j
dGlvbigpPQp7by5tb2RhbC5zaG93KHt0aXRsZToiQWRkIERhdGEgdG8gU2F2ZWQgVmlzdWFsaXph
dGlvbiIsYm9keTpzLGJ1dHRvbnM6e0NhbmM9CmVsOmZ1bmN0aW9uKCl7by5tb2RhbC5oaWRlKCl9
LCJBZGQgdG8gdmlzdWFsaXphdGlvbiI6ZnVuY3Rpb24oKXtvLm1vZGFsLmhpZD0KZSgpO28ubW9k
YWwuJGVsLmZpbmQoImlucHV0W25hbWU9M0RpZF06Y2hlY2tlZCIpLmVhY2goZnVuY3Rpb24oKXt2
YXIgdD0zRCQoPQp0aGlzKS52YWwoKTtxLmlkPTNEdDtvLmZyYW1lLmFkZCh7dGl0bGU6IlRyYWNr
c3RlciIsdHlwZToidXJsIixjb250ZW50Om4rIi89CnRyYWNrc3Rlcj8iKyQucGFyYW0ocSl9KX0p
fX19KX0sIlZpZXcgaW4gbmV3IHZpc3VhbGl6YXRpb24iOmZ1bmN0aW9uKCl7by5tbz0KZGFsLmhp
ZGUoKTtvLmZyYW1lLmFkZCh7dGl0bGU6IlRyYWNrc3RlciIsdHlwZToidXJsIixjb250ZW50Om4r
Ii90cmFja3N0ZXI/PQoiKyQucGFyYW0ocSl9KX19fSl9fSk7cmV0dXJuIGZhbHNlfX19KTt2YXIg
bD0zRGZ1bmN0aW9uKHEsbyxyLG4pe3ZhciBwPTNEbmU9Cncgbyh7bW9kZWw6bmV3IHEocil9KTtw
LnJlbmRlcigpO2lmKG4pe24uYXBwZW5kKHAuJGVsKX1yZXR1cm4gcH07dmFyIGM9M0RmdT0KbmN0
aW9uKHApe2lmKCFwLm1vZGVsKXtwLm1vZGVsPTNEbmV3IGkocC5kYXRhc2V0X2NvbmZpZyl9dmFy
IG89M0RwLnBhcmVudF9lPQpsdDt2YXIgcT0zRHAuZW1iZWRkZWQ7ZGVsZXRlIHAuZW1iZWRkZWQ7
ZGVsZXRlIHAucGFyZW50X2VsdDtkZWxldGUgcC5kYXRhc2U9CnRfY29uZmlnO3ZhciBuPTNEKHE/
bmV3IG0ocCk6bmV3IGcocCkpO24ucmVuZGVyKCk7aWYobyl7by5hcHBlbmQobi4kZWwpO24uZT0K
eHBhbmRfdG9fY29udGFpbmVyKCl9cmV0dXJuIG59O3JldHVybntEYXRhc2V0OmIsVGFidWxhckRh
dGFzZXQ6aSxEYXRhc2V0Q29sPQpsZWN0aW9uOmUsVGFidWxhckRhdGFzZXRDaHVua2VkVmlldzph
LGNyZWF0ZVRhYnVsYXJEYXRhc2V0Q2h1bmtlZFZpZXc6Y319KTsKXCBObyBuZXdsaW5lIGF0IGVu
ZCBvZiBmaWxlCgoKaHR0cHM6Ly9iaXRidWNrZXQub3JnL2dhbGF4eS9nYWxheHktY2VudHJhbC9j
b21taXRzLzM2NDU3Y2MxZWMyNi8KQ2hhbmdlc2V0OiAgIDM2NDU3Y2MxZWMyNgpVc2VyOiAgICAg
ICAgZGFubm9uCkRhdGU6ICAgICAgICAyMDE0LTEyLTE1IDE0OjQ3OjQ1KzAwOjAwClN1bW1hcnk6
ICAgICBTd2FwIHRhYnVsYXIgcmVuZGVyaW5nIGZyb20gdXNpbmcgaW5saW5lIGNzcyB0byBhcHBs
eWluZyBzdHlsZT0KcyBmb3Igcm93L2hlYWRlci4KQWZmZWN0ZWQgIzogIDUgZmlsZXMKCmRpZmYg
LXIgOGVjNjkwNTI2MDBhMmM5MTVmODhkMzE4NTg4NGExODVlODIwODZkMSAtciAzNjQ1N2NjMWVj
MjZjNDQwZTdhOTk2Yz0KZTUxZWI4N2VkN2M1OGNjNGQgY2xpZW50L2dhbGF4eS9zY3JpcHRzL212
Yy9kYXRhLmpzCi0tLSBhL2NsaWVudC9nYWxheHkvc2NyaXB0cy9tdmMvZGF0YS5qcworKysgYi9j
bGllbnQvZ2FsYXh5L3NjcmlwdHMvbXZjL2RhdGEuanMKQEAgLTEyOCwxMCArMTI4LDYgQEAKICAg
ICAgICAgdGhpcy5yb3dfY291bnQgPTNEIDA7CiAgICAgICAgIHRoaXMubG9hZGluZ19jaHVuayA9
M0QgZmFsc2U7Cj0yMAotICAgICAgICAvLyBDU1MgY29sb3JzIHVzZWQgaW4gdGFibGUuCi0gICAg
ICAgIHRoaXMuaGVhZGVyX2NvbG9yID0zRCAnI0FBQSc7Ci0gICAgICAgIHRoaXMuZGFya19yb3df
Y29sb3IgPTNEICcjREREJzsKLQogICAgICAgICAvLyBsb2FkIHRyYWNrc3RlciBidXR0b24KICAg
ICAgICAgbmV3IFRhYnVsYXJCdXR0b25UcmFja3N0ZXJWaWV3KHsKICAgICAgICAgICAgIG1vZGVs
ICAgOiBvcHRpb25zLm1vZGVsLApAQCAtMTczLDcgKzE2OSw3IEBACiAgICAgICAgIH0pOwogICAg
ICAgICB0aGlzLiRlbC5hcHBlbmQoZGF0YV90YWJsZSk7CiAgICAgICAgIHZhciBjb2x1bW5fbmFt
ZXMgPTNEIHRoaXMubW9kZWwuZ2V0X21ldGFkYXRhKCdjb2x1bW5fbmFtZXMnKSwKLSAgICAgICAg
ICAgIGhlYWRlcl9yb3cgPTNEICQoJzx0ci8+JykuY3NzKCdiYWNrZ3JvdW5kLWNvbG9yJywgdGhp
cy5oZWFkZXJfPQpjb2xvcikuYXBwZW5kVG8oZGF0YV90YWJsZSk7CisgICAgICAgICAgICBoZWFk
ZXJfcm93ID0zRCAkKCc8dHIvPicpLmFwcGVuZFRvKGRhdGFfdGFibGUpOwogICAgICAgICBpZiAo
Y29sdW1uX25hbWVzKSB7CiAgICAgICAgICAgICBoZWFkZXJfcm93LmFwcGVuZCgnPHRoPicgKyBj
b2x1bW5fbmFtZXMuam9pbignPC90aD48dGg+JykgKyAnPD0KL3RoPicpOwogICAgICAgICB9IGVs
c2UgewpAQCAtMjM2LDcgKzIzMiw3IEBACiAgICAgICAgICAgICBudW1fY29sdW1ucyA9M0QgdGhp
cy5tb2RlbC5nZXRfbWV0YWRhdGEoJ2NvbHVtbnMnKTsKPTIwCiAgICAgICAgIGlmICh0aGlzLnJv
d19jb3VudCAlIDIgIT0zRD0zRCAwKSB7Ci0gICAgICAgICAgICByb3cuY3NzKCdiYWNrZ3JvdW5k
LWNvbG9yJywgdGhpcy5kYXJrX3Jvd19jb2xvcik7CisgICAgICAgICAgICByb3cuYWRkQ2xhc3Mo
J2Rhcmtfcm93Jyk7CiAgICAgICAgIH0KPTIwCiAgICAgICAgIGlmIChjZWxscy5sZW5ndGggPTNE
PTNEPTNEIG51bV9jb2x1bW5zKSB7CgpkaWZmIC1yIDhlYzY5MDUyNjAwYTJjOTE1Zjg4ZDMxODU4
ODRhMTg1ZTgyMDg2ZDEgLXIgMzY0NTdjYzFlYzI2YzQ0MGU3YTk5NmM9CmU1MWViODdlZDdjNThj
YzRkIHN0YXRpYy9zY3JpcHRzL212Yy9kYXRhLmpzCi0tLSBhL3N0YXRpYy9zY3JpcHRzL212Yy9k
YXRhLmpzCisrKyBiL3N0YXRpYy9zY3JpcHRzL212Yy9kYXRhLmpzCkBAIC0xMjgsMTAgKzEyOCw2
IEBACiAgICAgICAgIHRoaXMucm93X2NvdW50ID0zRCAwOwogICAgICAgICB0aGlzLmxvYWRpbmdf
Y2h1bmsgPTNEIGZhbHNlOwo9MjAKLSAgICAgICAgLy8gQ1NTIGNvbG9ycyB1c2VkIGluIHRhYmxl
LgotICAgICAgICB0aGlzLmhlYWRlcl9jb2xvciA9M0QgJyNBQUEnOwotICAgICAgICB0aGlzLmRh
cmtfcm93X2NvbG9yID0zRCAnI0RERCc7Ci0KICAgICAgICAgLy8gbG9hZCB0cmFja3N0ZXIgYnV0
dG9uCiAgICAgICAgIG5ldyBUYWJ1bGFyQnV0dG9uVHJhY2tzdGVyVmlldyh7CiAgICAgICAgICAg
ICBtb2RlbCAgIDogb3B0aW9ucy5tb2RlbCwKQEAgLTE3Myw3ICsxNjksNyBAQAogICAgICAgICB9
KTsKICAgICAgICAgdGhpcy4kZWwuYXBwZW5kKGRhdGFfdGFibGUpOwogICAgICAgICB2YXIgY29s
dW1uX25hbWVzID0zRCB0aGlzLm1vZGVsLmdldF9tZXRhZGF0YSgnY29sdW1uX25hbWVzJyksCi0g
ICAgICAgICAgICBoZWFkZXJfcm93ID0zRCAkKCc8dHIvPicpLmNzcygnYmFja2dyb3VuZC1jb2xv
cicsIHRoaXMuaGVhZGVyXz0KY29sb3IpLmFwcGVuZFRvKGRhdGFfdGFibGUpOworICAgICAgICAg
ICAgaGVhZGVyX3JvdyA9M0QgJCgnPHRyLz4nKS5hcHBlbmRUbyhkYXRhX3RhYmxlKTsKICAgICAg
ICAgaWYgKGNvbHVtbl9uYW1lcykgewogICAgICAgICAgICAgaGVhZGVyX3Jvdy5hcHBlbmQoJzx0
aD4nICsgY29sdW1uX25hbWVzLmpvaW4oJzwvdGg+PHRoPicpICsgJzw9Ci90aD4nKTsKICAgICAg
ICAgfSBlbHNlIHsKQEAgLTIzNiw3ICsyMzIsNyBAQAogICAgICAgICAgICAgbnVtX2NvbHVtbnMg
PTNEIHRoaXMubW9kZWwuZ2V0X21ldGFkYXRhKCdjb2x1bW5zJyk7Cj0yMAogICAgICAgICBpZiAo
dGhpcy5yb3dfY291bnQgJSAyICE9M0Q9M0QgMCkgewotICAgICAgICAgICAgcm93LmNzcygnYmFj
a2dyb3VuZC1jb2xvcicsIHRoaXMuZGFya19yb3dfY29sb3IpOworICAgICAgICAgICAgcm93LmFk
ZENsYXNzKCdkYXJrX3JvdycpOwogICAgICAgICB9Cj0yMAogICAgICAgICBpZiAoY2VsbHMubGVu
Z3RoID0zRD0zRD0zRCBudW1fY29sdW1ucykgewoKZGlmZiAtciA4ZWM2OTA1MjYwMGEyYzkxNWY4
OGQzMTg1ODg0YTE4NWU4MjA4NmQxIC1yIDM2NDU3Y2MxZWMyNmM0NDBlN2E5OTZjPQplNTFlYjg3
ZWQ3YzU4Y2M0ZCBzdGF0aWMvc2NyaXB0cy9wYWNrZWQvbXZjL2RhdGEuanMKLS0tIGEvc3RhdGlj
L3NjcmlwdHMvcGFja2VkL212Yy9kYXRhLmpzCisrKyBiL3N0YXRpYy9zY3JpcHRzL3BhY2tlZC9t
dmMvZGF0YS5qcwpAQCAtMSwxICsxLDEgQEAKLWRlZmluZShbIm12Yy91aS91aS1tb2RhbCIsIm12
Yy91aS91aS1mcmFtZXMiLCJtdmMvdWkvaWNvbi1idXR0b24iXSxmdW5jdGlvPQpuKGssaixmKXt2
YXIgaD0zREJhY2tib25lLk1vZGVsLmV4dGVuZCh7fSk7dmFyIGI9M0RCYWNrYm9uZS5Nb2RlbC5l
eHRlbmQoe2Q9CmVmYXVsdHM6e2lkOiIiLHR5cGU6IiIsbmFtZToiIixoZGFfbGRkYToiaGRhIixt
ZXRhZGF0YTpudWxsfSxpbml0aWFsaXplOmZ1bj0KY3Rpb24oKXtpZighdGhpcy5nZXQoIm1ldGFk
YXRhIikpe3RoaXMuX3NldF9tZXRhZGF0YSgpfXRoaXMub24oImNoYW5nZSIsdGhpPQpzLl9zZXRf
bWV0YWRhdGEsdGhpcyl9LF9zZXRfbWV0YWRhdGE6ZnVuY3Rpb24oKXt2YXIgbj0zRG5ldyBoKCk7
Xy5lYWNoKF8ua2U9CnlzKHRoaXMuYXR0cmlidXRlcyksZnVuY3Rpb24obyl7aWYoby5pbmRleE9m
KCJtZXRhZGF0YV8iKT0zRD0zRD0zRDApe3ZhciBwPQo9M0RvLnNwbGl0KCJtZXRhZGF0YV8iKVsx
XTtuLnNldChwLHRoaXMuYXR0cmlidXRlc1tvXSk7ZGVsZXRlIHRoaXMuYXR0cmlidXQ9CmVzW29d
fX0sdGhpcyk7dGhpcy5zZXQoIm1ldGFkYXRhIixuLHtzaWxlbnQ6dHJ1ZX0pfSxnZXRfbWV0YWRh
dGE6ZnVuY3Rpb24obj0KKXtyZXR1cm4gdGhpcy5hdHRyaWJ1dGVzLm1ldGFkYXRhLmdldChuKX0s
dXJsUm9vdDpnYWxheHlfY29uZmlnLnJvb3QrImFwaS9kPQphdGFzZXRzIn0pO3ZhciBpPTNEYi5l
eHRlbmQoe2RlZmF1bHRzOl8uZXh0ZW5kKHt9LGIucHJvdG90eXBlLmRlZmF1bHRzLHtjaHU9Cm5r
X3VybDpudWxsLGZpcnN0X2RhdGFfY2h1bms6bnVsbCxjaHVua19pbmRleDotMSxhdF9lb2Y6ZmFs
c2V9KSxpbml0aWFsaXplOj0KZnVuY3Rpb24obil7Yi5wcm90b3R5cGUuaW5pdGlhbGl6ZS5jYWxs
KHRoaXMpO3RoaXMuYXR0cmlidXRlcy5jaHVua19pbmRleD0KPTNEKHRoaXMuYXR0cmlidXRlcy5m
aXJzdF9kYXRhX2NodW5rPzE6MCk7dGhpcy5hdHRyaWJ1dGVzLmNodW5rX3VybD0zRGdhbGF4PQp5
X2NvbmZpZy5yb290KyJkYXRhc2V0L2Rpc3BsYXk/ZGF0YXNldF9pZD0zRCIrdGhpcy5pZDt0aGlz
LmF0dHJpYnV0ZXMudXJsX3Y9Cml6PTNEZ2FsYXh5X2NvbmZpZy5yb290KyJ2aXN1YWxpemF0aW9u
In0sZ2V0X25leHRfY2h1bms6ZnVuY3Rpb24oKXtpZih0aGlzLj0KYXR0cmlidXRlcy5hdF9lb2Yp
e3JldHVybiBudWxsfXZhciBuPTNEdGhpcyxvPTNEJC5EZWZlcnJlZCgpOyQuZ2V0SlNPTih0aGlz
PQouYXR0cmlidXRlcy5jaHVua191cmwse2NodW5rOm4uYXR0cmlidXRlcy5jaHVua19pbmRleCsr
fSkuc3VjY2VzcyhmdW5jdGlvbig9CnApe3ZhciBxO2lmKHAuY2tfZGF0YSE9M0Q9M0QiIil7cT0z
RHB9ZWxzZXtuLmF0dHJpYnV0ZXMuYXRfZW9mPTNEdHJ1ZTtxPTNEbj0KdWxsfW8ucmVzb2x2ZShx
KX0pO3JldHVybiBvfX0pO3ZhciBlPTNEQmFja2JvbmUuQ29sbGVjdGlvbi5leHRlbmQoe21vZGVs
OmJ9PQopO3ZhciBhPTNEQmFja2JvbmUuVmlldy5leHRlbmQoe2luaXRpYWxpemU6ZnVuY3Rpb24o
bil7dGhpcy5yb3dfY291bnQ9M0QwO3Q9Cmhpcy5sb2FkaW5nX2NodW5rPTNEZmFsc2U7dGhpcy5o
ZWFkZXJfY29sb3I9M0QiI0FBQSI7dGhpcy5kYXJrX3Jvd19jb2xvcj0zRD0KIiNEREQiO25ldyBk
KHttb2RlbDpuLm1vZGVsLCRlbDp0aGlzLiRlbH0pfSxleHBhbmRfdG9fY29udGFpbmVyOmZ1bmN0
aW9uKCl7PQppZih0aGlzLiRlbC5oZWlnaHQoKTx0aGlzLnNjcm9sbF9lbHQuaGVpZ2h0KCkpe3Ro
aXMuYXR0ZW1wdF90b19mZXRjaCgpfX0sYXQ9CnRlbXB0X3RvX2ZldGNoOmZ1bmN0aW9uKG8pe3Zh
ciBuPTNEdGhpcztpZighdGhpcy5sb2FkaW5nX2NodW5rJiZ0aGlzLnNjcm9sbD0KZWRfdG9fYm90
dG9tKCkpe3RoaXMubG9hZGluZ19jaHVuaz0zRHRydWU7dGhpcy5sb2FkaW5nX2luZGljYXRvci5z
aG93KCk7JC53PQpoZW4obi5tb2RlbC5nZXRfbmV4dF9jaHVuaygpKS50aGVuKGZ1bmN0aW9uKHAp
e2lmKHApe24uX3JlbmRlckNodW5rKHApO24ubG89CmFkaW5nX2NodW5rPTNEZmFsc2V9bi5sb2Fk
aW5nX2luZGljYXRvci5oaWRlKCk7bi5leHBhbmRfdG9fY29udGFpbmVyKCl9KX19LD0KcmVuZGVy
OmZ1bmN0aW9uKCl7dGhpcy5sb2FkaW5nX2luZGljYXRvcj0zRCQoIjxkaXYvPiIpLmF0dHIoImlk
IiwibG9hZGluZ19pPQpuZGljYXRvciIpO3RoaXMuJGVsLmFwcGVuZCh0aGlzLmxvYWRpbmdfaW5k
aWNhdG9yKTt2YXIgcj0zRCQoIjx0YWJsZS8+IikuYXQ9CnRyKHtpZDoiY29udGVudF90YWJsZSIs
Y2VsbHBhZGRpbmc6MH0pO3RoaXMuJGVsLmFwcGVuZChyKTt2YXIgbj0zRHRoaXMubW9kZT0KbC5n
ZXRfbWV0YWRhdGEoImNvbHVtbl9uYW1lcyIpLHM9M0QkKCI8dHIvPiIpLmNzcygiYmFja2dyb3Vu
ZC1jb2xvciIsdGhpcy5oPQplYWRlcl9jb2xvcikuYXBwZW5kVG8ocik7aWYobil7cy5hcHBlbmQo
Ijx0aD4iK24uam9pbigiPC90aD48dGg+IikrIjwvdGg+Iik9Cn1lbHNle2Zvcih2YXIgcT0zRDE7
cTw9M0R0aGlzLm1vZGVsLmdldF9tZXRhZGF0YSgiY29sdW1ucyIpO3ErKyl7cy5hcHBlbmQoIj0K
PHRoPiIrcSsiPC90aD4iKX19dmFyIHA9M0R0aGlzLG89M0R0aGlzLm1vZGVsLmdldCgiZmlyc3Rf
ZGF0YV9jaHVuayIpO2lmKG8pPQp7dGhpcy5fcmVuZGVyQ2h1bmsobyl9ZWxzZXskLndoZW4ocC5t
b2RlbC5nZXRfbmV4dF9jaHVuaygpKS50aGVuKGZ1bmN0aW9uKHQ9Cil7cC5fcmVuZGVyQ2h1bmso
dCl9KX10aGlzLnNjcm9sbF9lbHQuc2Nyb2xsKGZ1bmN0aW9uKCl7cC5hdHRlbXB0X3RvX2ZldGNo
KD0KKX0pfSxzY3JvbGxlZF90b19ib3R0b206ZnVuY3Rpb24oKXtyZXR1cm4gZmFsc2V9LF9yZW5k
ZXJDZWxsOmZ1bmN0aW9uKHEsbixyPQope3ZhciBvPTNEJCgiPHRkPiIpLnRleHQocSk7dmFyIHA9
M0R0aGlzLm1vZGVsLmdldF9tZXRhZGF0YSgiY29sdW1uX3R5cGVzIik9CjtpZihyIT0zRD0zRHVu
ZGVmaW5lZCl7by5hdHRyKCJjb2xzcGFuIixyKS5hZGRDbGFzcygic3RyaW5nYWxpZ24iKX1lbHNl
e2lmKD0KcCl7aWYobjxwLmxlbmd0aCl7aWYocFtuXT0zRD0zRD0zRCJzdHIifHxwW25dPTNEPTNE
PTNEImxpc3QiKXtvLmFkZENsYXNzKCJzPQp0cmluZ2FsaWduIil9fX19cmV0dXJuIG99LF9yZW5k
ZXJSb3c6ZnVuY3Rpb24obil7dmFyIG89M0RuLnNwbGl0KCJcdCIpLHE9M0Q9CiQoIjx0cj4iKSxw
PTNEdGhpcy5tb2RlbC5nZXRfbWV0YWRhdGEoImNvbHVtbnMiKTtpZih0aGlzLnJvd19jb3VudCUy
IT0zRD0zRD0KMCl7cS5jc3MoImJhY2tncm91bmQtY29sb3IiLHRoaXMuZGFya19yb3dfY29sb3Ip
fWlmKG8ubGVuZ3RoPTNEPTNEPTNEcCl7Xy5lPQphY2gobyxmdW5jdGlvbihzLHIpe3EuYXBwZW5k
KHRoaXMuX3JlbmRlckNlbGwocyxyKSl9LHRoaXMpfWVsc2V7aWYoby5sZW5ndGg9Cj5wKXtfLmVh
Y2goby5zbGljZSgwLHAtMSksZnVuY3Rpb24ocyxyKXtxLmFwcGVuZCh0aGlzLl9yZW5kZXJDZWxs
KHMscikpfSx0aD0KaXMpO3EuYXBwZW5kKHRoaXMuX3JlbmRlckNlbGwoby5zbGljZShwLTEpLmpv
aW4oIlx0IikscC0xKSl9ZWxzZXtpZihwPjUmJm8uPQpsZW5ndGg9M0Q9M0Q9M0RwLTEpe18uZWFj
aChvLGZ1bmN0aW9uKHMscil7cS5hcHBlbmQodGhpcy5fcmVuZGVyQ2VsbChzLHIpKX09Cix0aGlz
KTtxLmFwcGVuZCgkKCI8dGQ+IikpfWVsc2V7cS5hcHBlbmQodGhpcy5fcmVuZGVyQ2VsbChuLDAs
cCkpfX19dGhpcy5ybz0Kd19jb3VudCsrO3JldHVybiBxfSxfcmVuZGVyQ2h1bms6ZnVuY3Rpb24o
bil7dmFyIG89M0R0aGlzLiRlbC5maW5kKCJ0YWJsZSIpPQo7Xy5lYWNoKG4uY2tfZGF0YS5zcGxp
dCgiXG4iKSxmdW5jdGlvbihwLHEpe2lmKHAhPTNEPTNEIiIpe28uYXBwZW5kKHRoaXMuX3I9CmVu
ZGVyUm93KHApKX19LHRoaXMpfX0pO3ZhciBnPTNEYS5leHRlbmQoe2luaXRpYWxpemU6ZnVuY3Rp
b24obil7YS5wcm90b3R5cD0KZS5pbml0aWFsaXplLmNhbGwodGhpcyxuKTtzY3JvbGxfZWx0PTNE
Xy5maW5kKHRoaXMuJGVsLnBhcmVudHMoKSxmdW5jdGlvbihvPQope3JldHVybiAkKG8pLmNzcygi
b3ZlcmZsb3ciKT0zRD0zRD0zRCJhdXRvIn0pO2lmKCFzY3JvbGxfZWx0KXtzY3JvbGxfZWx0PQo9
M0R3aW5kb3d9dGhpcy5zY3JvbGxfZWx0PTNEJChzY3JvbGxfZWx0KX0sc2Nyb2xsZWRfdG9fYm90
dG9tOmZ1bmN0aW9uKCl7cmU9CnR1cm4odGhpcy4kZWwuaGVpZ2h0KCktdGhpcy5zY3JvbGxfZWx0
LnNjcm9sbFRvcCgpLXRoaXMuc2Nyb2xsX2VsdC5oZWlnaHQoKT0KPD0zRDApfX0pO3ZhciBtPTNE
YS5leHRlbmQoe2luaXRpYWxpemU6ZnVuY3Rpb24obil7YS5wcm90b3R5cGUuaW5pdGlhbGl6ZS5j
PQphbGwodGhpcyxuKTt0aGlzLnNjcm9sbF9lbHQ9M0R0aGlzLiRlbC5jc3Moe3Bvc2l0aW9uOiJy
ZWxhdGl2ZSIsb3ZlcmZsb3c6InM9CmNyb2xsIixoZWlnaHQ6dGhpcy5vcHRpb25zLmhlaWdodHx8
IjUwMHB4In0pfSxzY3JvbGxlZF90b19ib3R0b206ZnVuY3Rpb24oKT0Ke3JldHVybiB0aGlzLiRl
bC5zY3JvbGxUb3AoKSt0aGlzLiRlbC5pbm5lckhlaWdodCgpPj0zRHRoaXMuZWwuc2Nyb2xsSGVp
Z2h0PQp9fSk7dmFyIGQ9M0RCYWNrYm9uZS5WaWV3LmV4dGVuZCh7Y29sOntjaHJvbTpudWxsLHN0
YXJ0Om51bGwsZW5kOm51bGx9LHVybF89CnZpejpudWxsLGRhdGFzZXRfaWQ6bnVsbCxnZW5vbWVf
YnVpbGQ6bnVsbCxmaWxlX2V4dDpudWxsLGluaXRpYWxpemU6ZnVuY3Rpbz0KbihwKXt2YXIgcz0z
RHBhcmVudC5HYWxheHk7aWYocyYmcy5tb2RhbCl7dGhpcy5tb2RhbD0zRHMubW9kYWx9aWYocyYm
cy5mcmFtPQplKXt0aGlzLmZyYW1lPTNEcy5mcmFtZX1pZighdGhpcy5tb2RhbHx8IXRoaXMuZnJh
bWUpe3JldHVybn12YXIgbz0zRHAubW9kZWw9Cjt2YXIgcj0zRG8uZ2V0KCJtZXRhZGF0YSIpO2lm
KCFvLmdldCgiZmlsZV9leHQiKSl7cmV0dXJufXRoaXMuZmlsZV9leHQ9M0RvLj0KZ2V0KCJmaWxl
X2V4dCIpO2lmKHRoaXMuZmlsZV9leHQ9M0Q9M0QiYmVkIil7aWYoci5nZXQoImNocm9tQ29sIikm
JnIuZ2V0KCJzPQp0YXJ0Q29sIikmJnIuZ2V0KCJlbmRDb2wiKSl7dGhpcy5jb2wuY2hyb209M0Ry
LmdldCgiY2hyb21Db2wiKS0xO3RoaXMuY29sLnM9CnRhcnQ9M0RyLmdldCgic3RhcnRDb2wiKS0x
O3RoaXMuY29sLmVuZD0zRHIuZ2V0KCJlbmRDb2wiKS0xfWVsc2V7Y29uc29sZS5sbz0KZygiVGFi
dWxhckJ1dHRvblRyYWNrc3RlciA6IEJlZC1maWxlIG1ldGFkYXRhIGluY29tcGxldGUuIik7cmV0
dXJufX1pZih0aGlzPQouZmlsZV9leHQ9M0Q9M0QidmNmIil7ZnVuY3Rpb24gcSh1LHYpe2Zvcih2
YXIgdD0zRDA7dDx2Lmxlbmd0aDt0Kyspe2lmKHZbdF09Ci5tYXRjaCh1KSl7cmV0dXJuIHR9fXJl
dHVybiAtMX10aGlzLmNvbC5jaHJvbT0zRHEoIkNocm9tIixyLmdldCgiY29sdW1uX25hbT0KZXMi
KSk7dGhpcy5jb2wuc3RhcnQ9M0RxKCJQb3MiLHIuZ2V0KCJjb2x1bW5fbmFtZXMiKSk7dGhpcy5j
b2wuZW5kPTNEbnVsbDtpPQpmKHRoaXMuY29sLmNocm9tPTNEPTNELTF8fHRoaXMuY29sLnN0YXJ0
PTNEPTNELTEpe2NvbnNvbGUubG9nKCJUYWJ1bGFyQnV0dG89Cm5UcmFja3N0ZXIgOiBWQ0YtZmls
ZSBtZXRhZGF0YSBpbmNvbXBsZXRlLiIpO3JldHVybn19aWYodGhpcy5jb2wuY2hyb209M0Q9Cj0z
RD0zRHVuZGVmaW5lZCl7cmV0dXJufWlmKG8uaWQpe3RoaXMuZGF0YXNldF9pZD0zRG8uaWR9ZWxz
ZXtjb25zb2xlLmxvZygiVD0KYWJ1bGFyQnV0dG9uVHJhY2tzdGVyIDogRGF0YXNldCBpZGVudGlm
aWNhdGlvbiBpcyBtaXNzaW5nLiIpO3JldHVybn1pZihvLmdlPQp0KCJ1cmxfdml6Iikpe3RoaXMu
dXJsX3Zpej0zRG8uZ2V0KCJ1cmxfdml6Iil9ZWxzZXtjb25zb2xlLmxvZygiVGFidWxhckJ1dHQ9
Cm9uVHJhY2tzdGVyIDogVXJsIGZvciB2aXN1YWxpemF0aW9uIGNvbnRyb2xsZXIgaXMgbWlzc2lu
Zy4iKTtyZXR1cm59aWYoby5nZT0KdCgiZ2Vub21lX2J1aWxkIikpe3RoaXMuZ2Vub21lX2J1aWxk
PTNEby5nZXQoImdlbm9tZV9idWlsZCIpfXZhciBuPTNEbmV3IGYuPQpJY29uQnV0dG9uVmlldyh7
bW9kZWw6bmV3IGYuSWNvbkJ1dHRvbih7dGl0bGU6IlZpc3VhbGl6ZSIsaWNvbl9jbGFzczoiY2hh
cnQ9Cl9jdXJ2ZSIsaWQ6ImJ0bl92aXoifSl9KTt0aGlzLnNldEVsZW1lbnQocC4kZWwpO3RoaXMu
JGVsLmFwcGVuZChuLnJlbmRlcigpLj0KJGVsKTt0aGlzLmhpZGUoKX0sZXZlbnRzOnsibW91c2Vv
dmVyIHRyIjoic2hvdyIsbW91c2VsZWF2ZToiaGlkZSJ9LHNob3c6ZnVuPQpjdGlvbihzKXtmdW5j
dGlvbiByKHgpe3JldHVybiAhaXNOYU4ocGFyc2VGbG9hdCh4KSkmJmlzRmluaXRlKHgpfWlmKHRo
aXMuY289CmwuY2hyb209M0Q9M0Q9M0RudWxsKXtyZXR1cm59dmFyIHc9M0QkKHMudGFyZ2V0KS5w
YXJlbnQoKTt2YXIgdD0zRHcuY2hpbGRyZT0KbigpLmVxKHRoaXMuY29sLmNocm9tKS5odG1sKCk7
dmFyIG49M0R3LmNoaWxkcmVuKCkuZXEodGhpcy5jb2wuc3RhcnQpLmh0bWwoPQopO3ZhciBwPTNE
dGhpcy5jb2wuZW5kP3cuY2hpbGRyZW4oKS5lcSh0aGlzLmNvbC5lbmQpLmh0bWwoKTpuO2lmKCF0
Lm1hdGNoKCI9Cl4jIikmJnQhPTNEPTNEIiImJnIobikpe3ZhciB2PTNEe2RhdGFzZXRfaWQ6dGhp
cy5kYXRhc2V0X2lkLGdlbmVfcmVnaW9uOnQrIj0KOiIrbisiLSIrcH07dmFyIHE9M0R3Lm9mZnNl
dCgpO3ZhciBvPTNEcS5sZWZ0LTEwO3ZhciB1PTNEcS50b3AtJCh3aW5kb3cpLnNjPQpyb2xsVG9w
KCkrMzskKCIjYnRuX3ZpeiIpLmNzcyh7cG9zaXRpb246ImZpeGVkIix0b3A6dSsicHgiLGxlZnQ6
bysicHgifSk7JCg9CiIjYnRuX3ZpeiIpLm9mZigiY2xpY2siKTskKCIjYnRuX3ZpeiIpLmNsaWNr
KHRoaXMuY3JlYXRlX3RyYWNrc3Rlcl9hY3Rpb24odD0KaGlzLnVybF92aXosdix0aGlzLmdlbm9t
ZV9idWlsZCkpOyQoIiNidG5fdml6Iikuc2hvdygpfWVsc2V7JCgiI2J0bl92aXoiKS5oPQppZGUo
KX19LGhpZGU6ZnVuY3Rpb24oKXt0aGlzLiRlbC5maW5kKCIjYnRuX3ZpeiIpLmhpZGUoKX0sY3Jl
YXRlX3RyYWNrc3Rlcl89CmFjdGlvbjpmdW5jdGlvbihuLHEscCl7dmFyIG89M0R0aGlzO3JldHVy
biBmdW5jdGlvbigpe3ZhciByPTNEe307aWYocCl7clsiZj0KLWRia2V5Il09M0RwfSQuYWpheCh7
dXJsOm4rIi9saXN0X3RyYWNrcz8iKyQucGFyYW0ociksZGF0YVR5cGU6Imh0bWwiLGVycm9yPQo6
ZnVuY3Rpb24oKXtvLm1vZGFsLnNob3coe3RpdGxlOiJTb21ldGhpbmcgd2VudCB3cm9uZyEiLGJv
ZHk6IlVuZm9ydHVuYXRlbHk9CiB3ZSBjb3VsZCBub3QgYWRkIHRoaXMgZGF0YXNldCB0byB0aGUg
dHJhY2sgYnJvd3Nlci4gUGxlYXNlIHRyeSBhZ2FpbiBvciBjbz0KbnRhY3QgdXMuIixidXR0b25z
OntDYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsLmhpZGUoKX19fSl9LHN1Y2Nlc3M6ZnVuY3Rpb24o
PQpzKXtvLm1vZGFsLnNob3coe3RpdGxlOiJWaWV3IERhdGEgaW4gYSBOZXcgb3IgU2F2ZWQgVmlz
dWFsaXphdGlvbiIsYnV0dG9uczo9CntDYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsLmhpZGUoKX0s
IlZpZXcgaW4gc2F2ZWQgdmlzdWFsaXphdGlvbiI6ZnVuY3Rpb24oKT0Ke28ubW9kYWwuc2hvdyh7
dGl0bGU6IkFkZCBEYXRhIHRvIFNhdmVkIFZpc3VhbGl6YXRpb24iLGJvZHk6cyxidXR0b25zOntD
YW5jPQplbDpmdW5jdGlvbigpe28ubW9kYWwuaGlkZSgpfSwiQWRkIHRvIHZpc3VhbGl6YXRpb24i
OmZ1bmN0aW9uKCl7by5tb2RhbC5oaWQ9CmUoKTtvLm1vZGFsLiRlbC5maW5kKCJpbnB1dFtuYW1l
PTNEaWRdOmNoZWNrZWQiKS5lYWNoKGZ1bmN0aW9uKCl7dmFyIHQ9M0QkKD0KdGhpcykudmFsKCk7
cS5pZD0zRHQ7by5mcmFtZS5hZGQoe3RpdGxlOiJUcmFja3N0ZXIiLHR5cGU6InVybCIsY29udGVu
dDpuKyIvPQp0cmFja3N0ZXI/IiskLnBhcmFtKHEpfSl9KX19fSl9LCJWaWV3IGluIG5ldyB2aXN1
YWxpemF0aW9uIjpmdW5jdGlvbigpe28ubW89CmRhbC5oaWRlKCk7by5mcmFtZS5hZGQoe3RpdGxl
OiJUcmFja3N0ZXIiLHR5cGU6InVybCIsY29udGVudDpuKyIvdHJhY2tzdGVyPz0KIiskLnBhcmFt
KHEpfSl9fX0pfX0pO3JldHVybiBmYWxzZX19fSk7dmFyIGw9M0RmdW5jdGlvbihxLG8scixuKXt2
YXIgcD0zRG5lPQp3IG8oe21vZGVsOm5ldyBxKHIpfSk7cC5yZW5kZXIoKTtpZihuKXtuLmFwcGVu
ZChwLiRlbCl9cmV0dXJuIHB9O3ZhciBjPTNEZnU9Cm5jdGlvbihwKXtpZighcC5tb2RlbCl7cC5t
b2RlbD0zRG5ldyBpKHAuZGF0YXNldF9jb25maWcpfXZhciBvPTNEcC5wYXJlbnRfZT0KbHQ7dmFy
IHE9M0RwLmVtYmVkZGVkO2RlbGV0ZSBwLmVtYmVkZGVkO2RlbGV0ZSBwLnBhcmVudF9lbHQ7ZGVs
ZXRlIHAuZGF0YXNlPQp0X2NvbmZpZzt2YXIgbj0zRChxP25ldyBtKHApOm5ldyBnKHApKTtuLnJl
bmRlcigpO2lmKG8pe28uYXBwZW5kKG4uJGVsKTtuLmU9CnhwYW5kX3RvX2NvbnRhaW5lcigpfXJl
dHVybiBufTtyZXR1cm57RGF0YXNldDpiLFRhYnVsYXJEYXRhc2V0OmksRGF0YXNldENvbD0KbGVj
dGlvbjplLFRhYnVsYXJEYXRhc2V0Q2h1bmtlZFZpZXc6YSxjcmVhdGVUYWJ1bGFyRGF0YXNldENo
dW5rZWRWaWV3OmN9fSk7ClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQorZGVmaW5lKFsibXZj
L3VpL3VpLW1vZGFsIiwibXZjL3VpL3VpLWZyYW1lcyIsIm12Yy91aS9pY29uLWJ1dHRvbiJdLGZ1
bmN0aW89Cm4oayxqLGYpe3ZhciBoPTNEQmFja2JvbmUuTW9kZWwuZXh0ZW5kKHt9KTt2YXIgYj0z
REJhY2tib25lLk1vZGVsLmV4dGVuZCh7ZD0KZWZhdWx0czp7aWQ6IiIsdHlwZToiIixuYW1lOiIi
LGhkYV9sZGRhOiJoZGEiLG1ldGFkYXRhOm51bGx9LGluaXRpYWxpemU6ZnVuPQpjdGlvbigpe2lm
KCF0aGlzLmdldCgibWV0YWRhdGEiKSl7dGhpcy5fc2V0X21ldGFkYXRhKCl9dGhpcy5vbigiY2hh
bmdlIix0aGk9CnMuX3NldF9tZXRhZGF0YSx0aGlzKX0sX3NldF9tZXRhZGF0YTpmdW5jdGlvbigp
e3ZhciBuPTNEbmV3IGgoKTtfLmVhY2goXy5rZT0KeXModGhpcy5hdHRyaWJ1dGVzKSxmdW5jdGlv
bihvKXtpZihvLmluZGV4T2YoIm1ldGFkYXRhXyIpPTNEPTNEPTNEMCl7dmFyIHA9Cj0zRG8uc3Bs
aXQoIm1ldGFkYXRhXyIpWzFdO24uc2V0KHAsdGhpcy5hdHRyaWJ1dGVzW29dKTtkZWxldGUgdGhp
cy5hdHRyaWJ1dD0KZXNbb119fSx0aGlzKTt0aGlzLnNldCgibWV0YWRhdGEiLG4se3NpbGVudDp0
cnVlfSl9LGdldF9tZXRhZGF0YTpmdW5jdGlvbihuPQope3JldHVybiB0aGlzLmF0dHJpYnV0ZXMu
bWV0YWRhdGEuZ2V0KG4pfSx1cmxSb290OmdhbGF4eV9jb25maWcucm9vdCsiYXBpL2Q9CmF0YXNl
dHMifSk7dmFyIGk9M0RiLmV4dGVuZCh7ZGVmYXVsdHM6Xy5leHRlbmQoe30sYi5wcm90b3R5cGUu
ZGVmYXVsdHMse2NodT0KbmtfdXJsOm51bGwsZmlyc3RfZGF0YV9jaHVuazpudWxsLGNodW5rX2lu
ZGV4Oi0xLGF0X2VvZjpmYWxzZX0pLGluaXRpYWxpemU6PQpmdW5jdGlvbihuKXtiLnByb3RvdHlw
ZS5pbml0aWFsaXplLmNhbGwodGhpcyk7dGhpcy5hdHRyaWJ1dGVzLmNodW5rX2luZGV4PQo9M0Qo
dGhpcy5hdHRyaWJ1dGVzLmZpcnN0X2RhdGFfY2h1bms/MTowKTt0aGlzLmF0dHJpYnV0ZXMuY2h1
bmtfdXJsPTNEZ2FsYXg9CnlfY29uZmlnLnJvb3QrImRhdGFzZXQvZGlzcGxheT9kYXRhc2V0X2lk
PTNEIit0aGlzLmlkO3RoaXMuYXR0cmlidXRlcy51cmxfdj0KaXo9M0RnYWxheHlfY29uZmlnLnJv
b3QrInZpc3VhbGl6YXRpb24ifSxnZXRfbmV4dF9jaHVuazpmdW5jdGlvbigpe2lmKHRoaXMuPQph
dHRyaWJ1dGVzLmF0X2VvZil7cmV0dXJuIG51bGx9dmFyIG49M0R0aGlzLG89M0QkLkRlZmVycmVk
KCk7JC5nZXRKU09OKHRoaXM9Ci5hdHRyaWJ1dGVzLmNodW5rX3VybCx7Y2h1bms6bi5hdHRyaWJ1
dGVzLmNodW5rX2luZGV4Kyt9KS5zdWNjZXNzKGZ1bmN0aW9uKD0KcCl7dmFyIHE7aWYocC5ja19k
YXRhIT0zRD0zRCIiKXtxPTNEcH1lbHNle24uYXR0cmlidXRlcy5hdF9lb2Y9M0R0cnVlO3E9M0Ru
PQp1bGx9by5yZXNvbHZlKHEpfSk7cmV0dXJuIG99fSk7dmFyIGU9M0RCYWNrYm9uZS5Db2xsZWN0
aW9uLmV4dGVuZCh7bW9kZWw6Yn09Cik7dmFyIGE9M0RCYWNrYm9uZS5WaWV3LmV4dGVuZCh7aW5p
dGlhbGl6ZTpmdW5jdGlvbihuKXt0aGlzLnJvd19jb3VudD0zRDA7dD0KaGlzLmxvYWRpbmdfY2h1
bms9M0RmYWxzZTtuZXcgZCh7bW9kZWw6bi5tb2RlbCwkZWw6dGhpcy4kZWx9KX0sZXhwYW5kX3Rv
X2NvPQpudGFpbmVyOmZ1bmN0aW9uKCl7aWYodGhpcy4kZWwuaGVpZ2h0KCk8dGhpcy5zY3JvbGxf
ZWx0LmhlaWdodCgpKXt0aGlzLmF0dGU9Cm1wdF90b19mZXRjaCgpfX0sYXR0ZW1wdF90b19mZXRj
aDpmdW5jdGlvbihvKXt2YXIgbj0zRHRoaXM7aWYoIXRoaXMubG9hZGluZz0KX2NodW5rJiZ0aGlz
LnNjcm9sbGVkX3RvX2JvdHRvbSgpKXt0aGlzLmxvYWRpbmdfY2h1bms9M0R0cnVlO3RoaXMubG9h
ZGluZ19pPQpuZGljYXRvci5zaG93KCk7JC53aGVuKG4ubW9kZWwuZ2V0X25leHRfY2h1bmsoKSku
dGhlbihmdW5jdGlvbihwKXtpZihwKXtuLl89CnJlbmRlckNodW5rKHApO24ubG9hZGluZ19jaHVu
az0zRGZhbHNlfW4ubG9hZGluZ19pbmRpY2F0b3IuaGlkZSgpO24uZXhwYW5kXz0KdG9fY29udGFp
bmVyKCl9KX19LHJlbmRlcjpmdW5jdGlvbigpe3RoaXMubG9hZGluZ19pbmRpY2F0b3I9M0QkKCI8
ZGl2Lz4iKS5hPQp0dHIoImlkIiwibG9hZGluZ19pbmRpY2F0b3IiKTt0aGlzLiRlbC5hcHBlbmQo
dGhpcy5sb2FkaW5nX2luZGljYXRvcik7dmFyIHI9Cj0zRCQoIjx0YWJsZS8+IikuYXR0cih7aWQ6
ImNvbnRlbnRfdGFibGUiLGNlbGxwYWRkaW5nOjB9KTt0aGlzLiRlbC5hcHBlbmQocj0KKTt2YXIg
bj0zRHRoaXMubW9kZWwuZ2V0X21ldGFkYXRhKCJjb2x1bW5fbmFtZXMiKSxzPTNEJCgiPHRyLz4i
KS5hcHBlbmRUbyhyPQopO2lmKG4pe3MuYXBwZW5kKCI8dGg+IituLmpvaW4oIjwvdGg+PHRoPiIp
KyI8L3RoPiIpfWVsc2V7Zm9yKHZhciBxPTNEMTtxPD0KPTNEdGhpcy5tb2RlbC5nZXRfbWV0YWRh
dGEoImNvbHVtbnMiKTtxKyspe3MuYXBwZW5kKCI8dGg+IitxKyI8L3RoPiIpfX12YXIgPQpwPTNE
dGhpcyxvPTNEdGhpcy5tb2RlbC5nZXQoImZpcnN0X2RhdGFfY2h1bmsiKTtpZihvKXt0aGlzLl9y
ZW5kZXJDaHVuayhvKX09CmVsc2V7JC53aGVuKHAubW9kZWwuZ2V0X25leHRfY2h1bmsoKSkudGhl
bihmdW5jdGlvbih0KXtwLl9yZW5kZXJDaHVuayh0KX0pfT0KdGhpcy5zY3JvbGxfZWx0LnNjcm9s
bChmdW5jdGlvbigpe3AuYXR0ZW1wdF90b19mZXRjaCgpfSl9LHNjcm9sbGVkX3RvX2JvdHRvPQpt
OmZ1bmN0aW9uKCl7cmV0dXJuIGZhbHNlfSxfcmVuZGVyQ2VsbDpmdW5jdGlvbihxLG4scil7dmFy
IG89M0QkKCI8dGQ+IikudGU9Cnh0KHEpO3ZhciBwPTNEdGhpcy5tb2RlbC5nZXRfbWV0YWRhdGEo
ImNvbHVtbl90eXBlcyIpO2lmKHIhPTNEPTNEdW5kZWZpbmVkKT0Ke28uYXR0cigiY29sc3BhbiIs
cikuYWRkQ2xhc3MoInN0cmluZ2FsaWduIil9ZWxzZXtpZihwKXtpZihuPHAubGVuZ3RoKXtpZihw
PQpbbl09M0Q9M0Q9M0Qic3RyInx8cFtuXT0zRD0zRD0zRCJsaXN0Iil7by5hZGRDbGFzcygic3Ry
aW5nYWxpZ24iKX19fX1yZXR1cm49CiBvfSxfcmVuZGVyUm93OmZ1bmN0aW9uKG4pe3ZhciBvPTNE
bi5zcGxpdCgiXHQiKSxxPTNEJCgiPHRyPiIpLHA9M0R0aGlzLm1vZD0KZWwuZ2V0X21ldGFkYXRh
KCJjb2x1bW5zIik7aWYodGhpcy5yb3dfY291bnQlMiE9M0Q9M0QwKXtxLmFkZENsYXNzKCJkYXJr
X3JvPQp3Iil9aWYoby5sZW5ndGg9M0Q9M0Q9M0RwKXtfLmVhY2gobyxmdW5jdGlvbihzLHIpe3Eu
YXBwZW5kKHRoaXMuX3JlbmRlckNlbGw9CihzLHIpKX0sdGhpcyl9ZWxzZXtpZihvLmxlbmd0aD5w
KXtfLmVhY2goby5zbGljZSgwLHAtMSksZnVuY3Rpb24ocyxyKXtxLmFwcD0KZW5kKHRoaXMuX3Jl
bmRlckNlbGwocyxyKSl9LHRoaXMpO3EuYXBwZW5kKHRoaXMuX3JlbmRlckNlbGwoby5zbGljZShw
LTEpLmpvPQppbigiXHQiKSxwLTEpKX1lbHNle2lmKHA+NSYmby5sZW5ndGg9M0Q9M0Q9M0RwLTEp
e18uZWFjaChvLGZ1bmN0aW9uKHMscil7cS49CmFwcGVuZCh0aGlzLl9yZW5kZXJDZWxsKHMscikp
fSx0aGlzKTtxLmFwcGVuZCgkKCI8dGQ+IikpfWVsc2V7cS5hcHBlbmQodGhpcz0KLl9yZW5kZXJD
ZWxsKG4sMCxwKSl9fX10aGlzLnJvd19jb3VudCsrO3JldHVybiBxfSxfcmVuZGVyQ2h1bms6ZnVu
Y3Rpb24obil7PQp2YXIgbz0zRHRoaXMuJGVsLmZpbmQoInRhYmxlIik7Xy5lYWNoKG4uY2tfZGF0
YS5zcGxpdCgiXG4iKSxmdW5jdGlvbihwLHEpe2k9CmYocCE9M0Q9M0QiIil7by5hcHBlbmQodGhp
cy5fcmVuZGVyUm93KHApKX19LHRoaXMpfX0pO3ZhciBnPTNEYS5leHRlbmQoe2luaT0KdGlhbGl6
ZTpmdW5jdGlvbihuKXthLnByb3RvdHlwZS5pbml0aWFsaXplLmNhbGwodGhpcyxuKTtzY3JvbGxf
ZWx0PTNEXy5maW5kPQoodGhpcy4kZWwucGFyZW50cygpLGZ1bmN0aW9uKG8pe3JldHVybiAkKG8p
LmNzcygib3ZlcmZsb3ciKT0zRD0zRD0zRCJhdXRvIn09Cik7aWYoIXNjcm9sbF9lbHQpe3Njcm9s
bF9lbHQ9M0R3aW5kb3d9dGhpcy5zY3JvbGxfZWx0PTNEJChzY3JvbGxfZWx0KX0sc2Nybz0KbGxl
ZF90b19ib3R0b206ZnVuY3Rpb24oKXtyZXR1cm4odGhpcy4kZWwuaGVpZ2h0KCktdGhpcy5zY3Jv
bGxfZWx0LnNjcm9sbFRvPQpwKCktdGhpcy5zY3JvbGxfZWx0LmhlaWdodCgpPD0zRDApfX0pO3Zh
ciBtPTNEYS5leHRlbmQoe2luaXRpYWxpemU6ZnVuY3Rpb249CihuKXthLnByb3RvdHlwZS5pbml0
aWFsaXplLmNhbGwodGhpcyxuKTt0aGlzLnNjcm9sbF9lbHQ9M0R0aGlzLiRlbC5jc3Moe3Bvcz0K
aXRpb246InJlbGF0aXZlIixvdmVyZmxvdzoic2Nyb2xsIixoZWlnaHQ6dGhpcy5vcHRpb25zLmhl
aWdodHx8IjUwMHB4In0pfSxzPQpjcm9sbGVkX3RvX2JvdHRvbTpmdW5jdGlvbigpe3JldHVybiB0
aGlzLiRlbC5zY3JvbGxUb3AoKSt0aGlzLiRlbC5pbm5lckhlaWc9Cmh0KCk+PTNEdGhpcy5lbC5z
Y3JvbGxIZWlnaHR9fSk7dmFyIGQ9M0RCYWNrYm9uZS5WaWV3LmV4dGVuZCh7Y29sOntjaHJvbTpu
dT0KbGwsc3RhcnQ6bnVsbCxlbmQ6bnVsbH0sdXJsX3ZpejpudWxsLGRhdGFzZXRfaWQ6bnVsbCxn
ZW5vbWVfYnVpbGQ6bnVsbCxmaWxlPQpfZXh0Om51bGwsaW5pdGlhbGl6ZTpmdW5jdGlvbihwKXt2
YXIgcz0zRHBhcmVudC5HYWxheHk7aWYocyYmcy5tb2RhbCl7dGhpcy49Cm1vZGFsPTNEcy5tb2Rh
bH1pZihzJiZzLmZyYW1lKXt0aGlzLmZyYW1lPTNEcy5mcmFtZX1pZighdGhpcy5tb2RhbHx8IXRo
aXMuZj0KcmFtZSl7cmV0dXJufXZhciBvPTNEcC5tb2RlbDt2YXIgcj0zRG8uZ2V0KCJtZXRhZGF0
YSIpO2lmKCFvLmdldCgiZmlsZV9leHQiPQopKXtyZXR1cm59dGhpcy5maWxlX2V4dD0zRG8uZ2V0
KCJmaWxlX2V4dCIpO2lmKHRoaXMuZmlsZV9leHQ9M0Q9M0QiYmVkIil7aWY9CihyLmdldCgiY2hy
b21Db2wiKSYmci5nZXQoInN0YXJ0Q29sIikmJnIuZ2V0KCJlbmRDb2wiKSl7dGhpcy5jb2wuY2hy
b209M0RyLj0KZ2V0KCJjaHJvbUNvbCIpLTE7dGhpcy5jb2wuc3RhcnQ9M0RyLmdldCgic3RhcnRD
b2wiKS0xO3RoaXMuY29sLmVuZD0zRHIuZ2V0PQooImVuZENvbCIpLTF9ZWxzZXtjb25zb2xlLmxv
ZygiVGFidWxhckJ1dHRvblRyYWNrc3RlciA6IEJlZC1maWxlIG1ldGFkYXRhIGk9Cm5jb21wbGV0
ZS4iKTtyZXR1cm59fWlmKHRoaXMuZmlsZV9leHQ9M0Q9M0QidmNmIil7ZnVuY3Rpb24gcSh1LHYp
e2Zvcih2YXIgdD0KPTNEMDt0PHYubGVuZ3RoO3QrKyl7aWYodlt0XS5tYXRjaCh1KSl7cmV0dXJu
IHR9fXJldHVybiAtMX10aGlzLmNvbC5jaHJvbT0KPTNEcSgiQ2hyb20iLHIuZ2V0KCJjb2x1bW5f
bmFtZXMiKSk7dGhpcy5jb2wuc3RhcnQ9M0RxKCJQb3MiLHIuZ2V0KCJjb2x1bW5fPQpuYW1lcyIp
KTt0aGlzLmNvbC5lbmQ9M0RudWxsO2lmKHRoaXMuY29sLmNocm9tPTNEPTNELTF8fHRoaXMuY29s
LnN0YXJ0PTNEPQo9M0QtMSl7Y29uc29sZS5sb2coIlRhYnVsYXJCdXR0b25UcmFja3N0ZXIgOiBW
Q0YtZmlsZSBtZXRhZGF0YSBpbmNvbXBsZXRlLiI9Cik7cmV0dXJufX1pZih0aGlzLmNvbC5jaHJv
bT0zRD0zRD0zRHVuZGVmaW5lZCl7cmV0dXJufWlmKG8uaWQpe3RoaXMuZGF0YXNldD0KX2lkPTNE
by5pZH1lbHNle2NvbnNvbGUubG9nKCJUYWJ1bGFyQnV0dG9uVHJhY2tzdGVyIDogRGF0YXNldCBp
ZGVudGlmaWNhdGlvPQpuIGlzIG1pc3NpbmcuIik7cmV0dXJufWlmKG8uZ2V0KCJ1cmxfdml6Iikp
e3RoaXMudXJsX3Zpej0zRG8uZ2V0KCJ1cmxfdml6Iik9Cn1lbHNle2NvbnNvbGUubG9nKCJUYWJ1
bGFyQnV0dG9uVHJhY2tzdGVyIDogVXJsIGZvciB2aXN1YWxpemF0aW9uIGNvbnRyb2xsZT0KciBp
cyBtaXNzaW5nLiIpO3JldHVybn1pZihvLmdldCgiZ2Vub21lX2J1aWxkIikpe3RoaXMuZ2Vub21l
X2J1aWxkPTNEby5nZXQoPQoiZ2Vub21lX2J1aWxkIil9dmFyIG49M0RuZXcgZi5JY29uQnV0dG9u
Vmlldyh7bW9kZWw6bmV3IGYuSWNvbkJ1dHRvbih7dGl0bGU9CjoiVmlzdWFsaXplIixpY29uX2Ns
YXNzOiJjaGFydF9jdXJ2ZSIsaWQ6ImJ0bl92aXoifSl9KTt0aGlzLnNldEVsZW1lbnQocC4kZT0K
bCk7dGhpcy4kZWwuYXBwZW5kKG4ucmVuZGVyKCkuJGVsKTt0aGlzLmhpZGUoKX0sZXZlbnRzOnsi
bW91c2VvdmVyIHRyIjoic2hvPQp3Iixtb3VzZWxlYXZlOiJoaWRlIn0sc2hvdzpmdW5jdGlvbihz
KXtmdW5jdGlvbiByKHgpe3JldHVybiAhaXNOYU4ocGFyc2VGbG89CmF0KHgpKSYmaXNGaW5pdGUo
eCl9aWYodGhpcy5jb2wuY2hyb209M0Q9M0Q9M0RudWxsKXtyZXR1cm59dmFyIHc9M0QkKHMudGFy
Zz0KZXQpLnBhcmVudCgpO3ZhciB0PTNEdy5jaGlsZHJlbigpLmVxKHRoaXMuY29sLmNocm9tKS5o
dG1sKCk7dmFyIG49M0R3LmNoaWxkPQpyZW4oKS5lcSh0aGlzLmNvbC5zdGFydCkuaHRtbCgpO3Zh
ciBwPTNEdGhpcy5jb2wuZW5kP3cuY2hpbGRyZW4oKS5lcSh0aGlzLmM9Cm9sLmVuZCkuaHRtbCgp
Om47aWYoIXQubWF0Y2goIl4jIikmJnQhPTNEPTNEIiImJnIobikpe3ZhciB2PTNEe2RhdGFzZXRf
aWQ6dD0KaGlzLmRhdGFzZXRfaWQsZ2VuZV9yZWdpb246dCsiOiIrbisiLSIrcH07dmFyIHE9M0R3
Lm9mZnNldCgpO3ZhciBvPTNEcS5sZWZ0PQotMTA7dmFyIHU9M0RxLnRvcC0kKHdpbmRvdykuc2Ny
b2xsVG9wKCkrMzskKCIjYnRuX3ZpeiIpLmNzcyh7cG9zaXRpb246ImZpeGU9CmQiLHRvcDp1KyJw
eCIsbGVmdDpvKyJweCJ9KTskKCIjYnRuX3ZpeiIpLm9mZigiY2xpY2siKTskKCIjYnRuX3ZpeiIp
LmNsaWNrKD0KdGhpcy5jcmVhdGVfdHJhY2tzdGVyX2FjdGlvbih0aGlzLnVybF92aXosdix0aGlz
Lmdlbm9tZV9idWlsZCkpOyQoIiNidG5fdml6PQoiKS5zaG93KCl9ZWxzZXskKCIjYnRuX3ZpeiIp
LmhpZGUoKX19LGhpZGU6ZnVuY3Rpb24oKXt0aGlzLiRlbC5maW5kKCIjYnRuX3Y9Cml6IikuaGlk
ZSgpfSxjcmVhdGVfdHJhY2tzdGVyX2FjdGlvbjpmdW5jdGlvbihuLHEscCl7dmFyIG89M0R0aGlz
O3JldHVybiBmdT0KbmN0aW9uKCl7dmFyIHI9M0R7fTtpZihwKXtyWyJmLWRia2V5Il09M0RwfSQu
YWpheCh7dXJsOm4rIi9saXN0X3RyYWNrcz8iKyQuPQpwYXJhbShyKSxkYXRhVHlwZToiaHRtbCIs
ZXJyb3I6ZnVuY3Rpb24oKXtvLm1vZGFsLnNob3coe3RpdGxlOiJTb21ldGhpbmcgd2U9Cm50IHdy
b25nISIsYm9keToiVW5mb3J0dW5hdGVseSB3ZSBjb3VsZCBub3QgYWRkIHRoaXMgZGF0YXNldCB0
byB0aGUgdHJhY2sgYj0Kcm93c2VyLiBQbGVhc2UgdHJ5IGFnYWluIG9yIGNvbnRhY3QgdXMuIixi
dXR0b25zOntDYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsPQouaGlkZSgpfX19KX0sc3VjY2Vzczpm
dW5jdGlvbihzKXtvLm1vZGFsLnNob3coe3RpdGxlOiJWaWV3IERhdGEgaW4gYSBOZXcgb3I9CiBT
YXZlZCBWaXN1YWxpemF0aW9uIixidXR0b25zOntDYW5jZWw6ZnVuY3Rpb24oKXtvLm1vZGFsLmhp
ZGUoKX0sIlZpZXcgaW4gcz0KYXZlZCB2aXN1YWxpemF0aW9uIjpmdW5jdGlvbigpe28ubW9kYWwu
c2hvdyh7dGl0bGU6IkFkZCBEYXRhIHRvIFNhdmVkIFZpc3VhPQpsaXphdGlvbiIsYm9keTpzLGJ1
dHRvbnM6e0NhbmNlbDpmdW5jdGlvbigpe28ubW9kYWwuaGlkZSgpfSwiQWRkIHRvIHZpc3VhbGk9
CnphdGlvbiI6ZnVuY3Rpb24oKXtvLm1vZGFsLmhpZGUoKTtvLm1vZGFsLiRlbC5maW5kKCJpbnB1
dFtuYW1lPTNEaWRdOmNoZWNrZT0KZCIpLmVhY2goZnVuY3Rpb24oKXt2YXIgdD0zRCQodGhpcyku
dmFsKCk7cS5pZD0zRHQ7by5mcmFtZS5hZGQoe3RpdGxlOiJUcmFjPQprc3RlciIsdHlwZToidXJs
Iixjb250ZW50Om4rIi90cmFja3N0ZXI/IiskLnBhcmFtKHEpfSl9KX19fSl9LCJWaWV3IGluIG5l
dyA9CnZpc3VhbGl6YXRpb24iOmZ1bmN0aW9uKCl7by5tb2RhbC5oaWRlKCk7by5mcmFtZS5hZGQo
e3RpdGxlOiJUcmFja3N0ZXIiLHR5cD0KZToidXJsIixjb250ZW50Om4rIi90cmFja3N0ZXI/Iisk
LnBhcmFtKHEpfSl9fX0pfX0pO3JldHVybiBmYWxzZX19fSk7dmFyIGw9Cj0zRGZ1bmN0aW9uKHEs
byxyLG4pe3ZhciBwPTNEbmV3IG8oe21vZGVsOm5ldyBxKHIpfSk7cC5yZW5kZXIoKTtpZihuKXtu
LmFwcD0KZW5kKHAuJGVsKX1yZXR1cm4gcH07dmFyIGM9M0RmdW5jdGlvbihwKXtpZighcC5tb2Rl
bCl7cC5tb2RlbD0zRG5ldyBpKHAuZGF0PQphc2V0X2NvbmZpZyl9dmFyIG89M0RwLnBhcmVudF9l
bHQ7dmFyIHE9M0RwLmVtYmVkZGVkO2RlbGV0ZSBwLmVtYmVkZGVkO2RlbGU9CnRlIHAucGFyZW50
X2VsdDtkZWxldGUgcC5kYXRhc2V0X2NvbmZpZzt2YXIgbj0zRChxP25ldyBtKHApOm5ldyBnKHAp
KTtuLnJlbj0KZGVyKCk7aWYobyl7by5hcHBlbmQobi4kZWwpO24uZXhwYW5kX3RvX2NvbnRhaW5l
cigpfXJldHVybiBufTtyZXR1cm57RGF0YXNlPQp0OmIsVGFidWxhckRhdGFzZXQ6aSxEYXRhc2V0
Q29sbGVjdGlvbjplLFRhYnVsYXJEYXRhc2V0Q2h1bmtlZFZpZXc6YSxjcmVhdGU9ClRhYnVsYXJE
YXRhc2V0Q2h1bmtlZFZpZXc6Y319KTsKXCBObyBuZXdsaW5lIGF0IGVuZCBvZiBmaWxlCgpkaWZm
IC1yIDhlYzY5MDUyNjAwYTJjOTE1Zjg4ZDMxODU4ODRhMTg1ZTgyMDg2ZDEgLXIgMzY0NTdjYzFl
YzI2YzQ0MGU3YTk5NmM9CmU1MWViODdlZDdjNThjYzRkIHN0YXRpYy9zdHlsZS9ibHVlL2Jhc2Uu
Y3NzCi0tLSBhL3N0YXRpYy9zdHlsZS9ibHVlL2Jhc2UuY3NzCisrKyBiL3N0YXRpYy9zdHlsZS9i
bHVlL2Jhc2UuY3NzCkBAIC0yMjM0LDYgKzIyMzQsOCBAQAogI2xvYWRpbmdfaW5kaWNhdG9ye3Bv
c2l0aW9uOmZpeGVkO3JpZ2h0OjEwcHg7dG9wOjEwcHg7aGVpZ2h0OjMycHg7d2lkdGg6MzI9CnB4
O2Rpc3BsYXk6bm9uZTtiYWNrZ3JvdW5kOnVybChsYXJnZXNwaW5uZXIuZ2lmKX0KICNjb250ZW50
X3RhYmxlIHRke3RleHQtYWxpZ246cmlnaHQ7d2hpdGUtc3BhY2U6bm93cmFwO3BhZGRpbmc6MnB4
IDEwcHh9CiAjY29udGVudF90YWJsZSB0ZC5zdHJpbmdhbGlnbnt0ZXh0LWFsaWduOmxlZnR9Cisj
Y29udGVudF90YWJsZSAuZGFya19yb3d7YmFja2dyb3VuZC1jb2xvcjojREREfQorI2NvbnRlbnRf
dGFibGUgdGh7YmFja2dyb3VuZC1jb2xvcjojQUFBfQogLnRvb2xNZW51QW5kVmlldyAudG9vbEZv
cm17ZmxvYXQ6bGVmdDtiYWNrZ3JvdW5kLWNvbG9yOndoaXRlO21hcmdpbjoxMHB4fQogLnRleHQt
YW5kLWF1dG9jb21wbGV0ZS1zZWxlY3R7YmFja2dyb3VuZDpub25lO3Bvc2l0aW9uOnJlbGF0aXZl
O3BhZGRpbmctcmk9CmdodDoxOHB4fS50ZXh0LWFuZC1hdXRvY29tcGxldGUtc2VsZWN0OmFmdGVy
e21hcmdpbi10b3A6NnB4O3Bvc2l0aW9uOmFic29sdT0KdGU7dG9wOjJweDtyaWdodDo2cHg7d2lk
dGg6MTBweDtoZWlnaHQ6MTBweDtkaXNwbGF5OmlubGluZS1ibG9jazt3aWR0aDowO2hlPQppZ2h0
OjA7bWFyZ2luLWxlZnQ6MnB4O3ZlcnRpY2FsLWFsaWduOm1pZGRsZTtib3JkZXItdG9wOjRweCBz
b2xpZCAjMDAwO2JvcmQ9CmVyLXJpZ2h0OjRweCBzb2xpZCB0cmFuc3BhcmVudDtib3JkZXItbGVm
dDo0cHggc29saWQgdHJhbnNwYXJlbnQ7Ym9yZGVyLWJvdD0KdG9tOjAgZG90dGVkO2NvbnRlbnQ6
IiI7ZmlsdGVyOmFscGhhKG9wYWNpdHk9M0Q4MDAwKTtvcGFjaXR5OjgwOy1tcy1maWx0ZXI6PQpw
cm9naWQ6RFhJbWFnZVRyYW5zZm9ybS5NaWNyb3NvZnQuQWxwaGEoT3BhY2l0eT0zRDgwMDApO2Zp
bHRlcjphbHBoYShvcGFjaXQ9Cnk9M0Q4MDAwKX0uYnRuLWRlZmF1bHQgLnRleHQtYW5kLWF1dG9j
b21wbGV0ZS1zZWxlY3Q6YWZ0ZXJ7Ym9yZGVyLXRvcC1jb2xvcj0KOiMzMzN9CiAuYnRuLXByaW1h
cnkgLnRleHQtYW5kLWF1dG9jb21wbGV0ZS1zZWxlY3Q6YWZ0ZXIsLmJ0bi1zdWNjZXNzIC50ZXh0
LWFuZC1hdT0KdG9jb21wbGV0ZS1zZWxlY3Q6YWZ0ZXIsLmJ0bi13YXJuaW5nIC50ZXh0LWFuZC1h
dXRvY29tcGxldGUtc2VsZWN0OmFmdGVyLC5iPQp0bi1kYW5nZXIgLnRleHQtYW5kLWF1dG9jb21w
bGV0ZS1zZWxlY3Q6YWZ0ZXIsLmJ0bi1pbmZvIC50ZXh0LWFuZC1hdXRvY29tcGw9CmV0ZS1zZWxl
Y3Q6YWZ0ZXJ7Ym9yZGVyLXRvcC1jb2xvcjojZmZmfQoKZGlmZiAtciA4ZWM2OTA1MjYwMGEyYzkx
NWY4OGQzMTg1ODg0YTE4NWU4MjA4NmQxIC1yIDM2NDU3Y2MxZWMyNmM0NDBlN2E5OTZjPQplNTFl
Yjg3ZWQ3YzU4Y2M0ZCBzdGF0aWMvc3R5bGUvc3JjL2xlc3MvYmFzZS5sZXNzCi0tLSBhL3N0YXRp
Yy9zdHlsZS9zcmMvbGVzcy9iYXNlLmxlc3MKKysrIGIvc3RhdGljL3N0eWxlL3NyYy9sZXNzL2Jh
c2UubGVzcwpAQCAtMTU1Myw2ICsxNTUzLDE0IEBACiAgICAgdGV4dC1hbGlnbjpsZWZ0OwogfQo9
MjAKKyNjb250ZW50X3RhYmxlIC5kYXJrX3Jvd3sKKyAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjRERE
OworfQorCisjY29udGVudF90YWJsZSB0aHsKKyAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjQUFBOwor
fQorCiAvLyA9M0Q9M0Q9M0Q9M0QgSW50ZWdyYXRlZCB0b29sIGZvcm0gc3R5bGVzCj0yMAogLnRv
b2xNZW51QW5kVmlldyAudG9vbEZvcm0KClJlcG9zaXRvcnkgVVJMOiBodHRwczovL2JpdGJ1Y2tl
dC5vcmcvZ2FsYXh5L2dhbGF4eS1jZW50cmFsLwoKLS0KClRoaXMgaXMgYSBjb21taXQgbm90aWZp
Y2F0aW9uIGZyb20gYml0YnVja2V0Lm9yZy4gWW91IGFyZSByZWNlaXZpbmcKdGhpcyBiZWNhdXNl
IHlvdSBoYXZlIHRoZSBzZXJ2aWNlIGVuYWJsZWQsIGFkZHJlc3NpbmcgdGhlIHJlY2lwaWVudCBv
Zgp0aGlzIGVtYWlsLgo
1
0
commit/galaxy-central: dannon: Organize and deduplicate imports, pep8 in lib/galaxy/config.py
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/7e68aa3d8d16/
Changeset: 7e68aa3d8d16
User: dannon
Date: 2014-12-15 13:28:21+00:00
Summary: Organize and deduplicate imports, pep8 in lib/galaxy/config.py
Affected #: 1 file
diff -r 5223de4eacea84dc2f39ec1ecec33ce45fa84739 -r 7e68aa3d8d161ff2a0526101aedebc026b4d71cc lib/galaxy/config.py
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -4,23 +4,22 @@
# absolute_import needed for tool_shed package.
from __future__ import absolute_import
+import ConfigParser
+import logging
+import logging.config
import os
import re
import socket
import string
import sys
import tempfile
-import logging
-import logging.config
-import ConfigParser
from datetime import timedelta
+from galaxy import eggs
+from galaxy.util import listify
+from galaxy.util import string_as_bool
+from galaxy.util.dbkeys import GenomeBuilds
from galaxy.web.formatting import expand_pretty_datetime_format
-from galaxy.util import string_as_bool
-from galaxy.util import listify
-from galaxy.util.dbkeys import GenomeBuilds
-from galaxy import eggs
-import ConfigParser
log = logging.getLogger( __name__ )
@@ -442,23 +441,23 @@
Backwards compatibility for config files moved to the config/ dir.
"""
defaults = dict(
- data_manager_config_file = [ 'config/data_manager_conf.xml', 'data_manager_conf.xml', 'config/data_manager_conf.xml.sample' ],
- datatypes_config_file = [ 'config/datatypes_conf.xml', 'datatypes_conf.xml', 'config/datatypes_conf.xml.sample' ],
- external_service_type_config_file = [ 'config/external_service_types_conf.xml', 'external_service_types_conf.xml', 'config/external_service_types_conf.xml.sample' ],
- job_config_file = [ 'config/job_conf.xml', 'job_conf.xml' ],
- job_metrics_config_file = [ 'config/job_metrics_conf.xml', 'job_metrics_conf.xml' ],
- dependency_resolvers_config_file = [ 'config/dependency_resolvers_conf.xml', 'dependency_resolvers_conf.xml' ],
- job_resource_params_file = [ 'config/job_resource_params_conf.xml', 'job_resource_params_conf.xml' ],
- migrated_tools_config = [ 'migrated_tools_conf.xml', 'config/migrated_tools_conf.xml' ],
- object_store_config_file = [ 'config/object_store_conf.xml', 'object_store_conf.xml' ],
- openid_config_file = [ 'config/openid_conf.xml', 'openid_conf.xml', 'config/openid_conf.xml.sample' ],
- shed_data_manager_config_file = [ 'shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml' ],
- shed_tool_data_table_config = [ 'shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml' ],
- tool_sheds_config_file = [ 'config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample' ],
+ data_manager_config_file=[ 'config/data_manager_conf.xml', 'data_manager_conf.xml', 'config/data_manager_conf.xml.sample' ],
+ datatypes_config_file=[ 'config/datatypes_conf.xml', 'datatypes_conf.xml', 'config/datatypes_conf.xml.sample' ],
+ external_service_type_config_file=[ 'config/external_service_types_conf.xml', 'external_service_types_conf.xml', 'config/external_service_types_conf.xml.sample' ],
+ job_config_file=[ 'config/job_conf.xml', 'job_conf.xml' ],
+ job_metrics_config_file=[ 'config/job_metrics_conf.xml', 'job_metrics_conf.xml' ],
+ dependency_resolvers_config_file=[ 'config/dependency_resolvers_conf.xml', 'dependency_resolvers_conf.xml' ],
+ job_resource_params_file=[ 'config/job_resource_params_conf.xml', 'job_resource_params_conf.xml' ],
+ migrated_tools_config=[ 'migrated_tools_conf.xml', 'config/migrated_tools_conf.xml' ],
+ object_store_config_file=[ 'config/object_store_conf.xml', 'object_store_conf.xml' ],
+ openid_config_file=[ 'config/openid_conf.xml', 'openid_conf.xml', 'config/openid_conf.xml.sample' ],
+ shed_data_manager_config_file=[ 'shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml' ],
+ shed_tool_data_table_config=[ 'shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml' ],
+ tool_sheds_config_file=[ 'config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample' ],
)
listify_defaults = dict(
- tool_data_table_config_path = [ 'config/tool_data_table_conf.xml', 'tool_data_table_conf.xml', 'config/tool_data_table_conf.xml.sample' ],
+ tool_data_table_config_path=[ 'config/tool_data_table_conf.xml', 'tool_data_table_conf.xml', 'config/tool_data_table_conf.xml.sample' ],
# rationale:
# [0]: user has explicitly created config/tool_conf.xml but did not
# move their existing shed_tool_conf.xml, don't use
@@ -469,7 +468,7 @@
# [0], probably moved their shed_tool_conf.xml as well
# [2]: user has done nothing, use the old files
# [3]: fresh install
- tool_config_file = [ 'config/tool_conf.xml,shed_tool_conf.xml', 'config/tool_conf.xml,config/shed_tool_conf.xml', 'tool_conf.xml,shed_tool_conf.xml', 'config/tool_conf.xml.sample,config/shed_tool_conf.xml' ]
+ tool_config_file=[ 'config/tool_conf.xml,shed_tool_conf.xml', 'config/tool_conf.xml,config/shed_tool_conf.xml', 'tool_conf.xml,shed_tool_conf.xml', 'config/tool_conf.xml.sample,config/shed_tool_conf.xml' ]
)
for var, defaults in defaults.items():
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
0
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/5223de4eacea/
Changeset: 5223de4eacea
User: dannon
Date: 2014-12-15 13:25:15+00:00
Summary: Merge.
Affected #: 1 file
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
0
2 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/6704c0ee9fec/
Changeset: 6704c0ee9fec
Branch: next-stable
User: dannon
Date: 2014-12-15 13:23:47+00:00
Summary: Bugfix for openid configuration locations, pointed out by Jelle in https://trello.com/c/AwZwAx4l/2289-lib-galaxy-config-py-missing-comma-on-op…
Affected #: 1 file
diff -r 7a027f285f3b441f8ff1fe48dfc5841d8bc41bfa -r 6704c0ee9fec6b0624d739669cb7c66ea08d480a lib/galaxy/config.py
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -445,7 +445,7 @@
job_resource_params_file = [ 'config/job_resource_params_conf.xml', 'job_resource_params_conf.xml' ],
migrated_tools_config = [ 'migrated_tools_conf.xml', 'config/migrated_tools_conf.xml' ],
object_store_config_file = [ 'config/object_store_conf.xml', 'object_store_conf.xml' ],
- openid_config_file = [ 'config/openid_conf.xml', 'openid_conf.xml' 'config/openid_conf.xml.sample' ],
+ openid_config_file = [ 'config/openid_conf.xml', 'openid_conf.xml', 'config/openid_conf.xml.sample' ],
shed_data_manager_config_file = [ 'shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml' ],
shed_tool_data_table_config = [ 'shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml' ],
tool_sheds_config_file = [ 'config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample' ],
https://bitbucket.org/galaxy/galaxy-central/commits/6fee59d63b7e/
Changeset: 6fee59d63b7e
User: dannon
Date: 2014-12-15 13:23:47+00:00
Summary: Bugfix for openid configuration locations, pointed out by Jelle in https://trello.com/c/AwZwAx4l/2289-lib-galaxy-config-py-missing-comma-on-op…
Affected #: 1 file
diff -r ad28e42a8af34c81f0ebcbe6f3baef9bdcf80ae8 -r 6fee59d63b7e042851b2a63e4d66ad6dde630e3b lib/galaxy/config.py
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -451,7 +451,7 @@
job_resource_params_file = [ 'config/job_resource_params_conf.xml', 'job_resource_params_conf.xml' ],
migrated_tools_config = [ 'migrated_tools_conf.xml', 'config/migrated_tools_conf.xml' ],
object_store_config_file = [ 'config/object_store_conf.xml', 'object_store_conf.xml' ],
- openid_config_file = [ 'config/openid_conf.xml', 'openid_conf.xml' 'config/openid_conf.xml.sample' ],
+ openid_config_file = [ 'config/openid_conf.xml', 'openid_conf.xml', 'config/openid_conf.xml.sample' ],
shed_data_manager_config_file = [ 'shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml' ],
shed_tool_data_table_config = [ 'shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml' ],
tool_sheds_config_file = [ 'config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample' ],
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
0
commit/galaxy-central: dannon: Merged in jjohnson/galaxy-central-7 (pull request #609)
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/ad28e42a8af3/
Changeset: ad28e42a8af3
User: dannon
Date: 2014-12-15 12:43:42+00:00
Summary: Merged in jjohnson/galaxy-central-7 (pull request #609)
Add dataproviders for SQlite datatype
Affected #: 2 files
diff -r ecac8a8679612d419347dcbedc398319418b077e -r ad28e42a8af34c81f0ebcbe6f3baef9bdcf80ae8 lib/galaxy/datatypes/binary.py
--- a/lib/galaxy/datatypes/binary.py
+++ b/lib/galaxy/datatypes/binary.py
@@ -620,6 +620,16 @@
dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
return dataproviders.dataset.SQliteDataProvider( dataset_source, **settings )
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-table', dataproviders.dataset.SQliteDataTableProvider.settings )
+ def sqlite_datatableprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataTableProvider( dataset_source, **settings )
+
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-dict', dataproviders.dataset.SQliteDataDictProvider.settings )
+ def sqlite_datadictprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataDictProvider( dataset_source, **settings )
+
Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
diff -r ecac8a8679612d419347dcbedc398319418b077e -r ad28e42a8af34c81f0ebcbe6f3baef9bdcf80ae8 lib/galaxy/datatypes/dataproviders/dataset.py
--- a/lib/galaxy/datatypes/dataproviders/dataset.py
+++ b/lib/galaxy/datatypes/dataproviders/dataset.py
@@ -13,6 +13,7 @@
import external
from galaxy.util import sqlite
import re
+import sys
from galaxy import eggs
eggs.require( 'bx-python' )
@@ -726,3 +727,55 @@
else:
yield
+class SQliteDataTableProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of arrays
+ """
+ settings = {
+ 'query': 'str',
+ 'headers': 'bool',
+ 'limit': 'int'
+ }
+
+ def __init__( self, source, query=None, headers=False, limit=sys.maxint, **kwargs ):
+ self.query = query
+ self.headers = headers
+ self.limit = limit
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataTableProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ results = cur.execute(self.query)
+ if self.headers:
+ yield [col[0] for col in cur.description]
+ for i,row in enumerate(results):
+ if i >= self.limit:
+ break
+ yield [val for val in row]
+ else:
+ yield
+
+class SQliteDataDictProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of dicts
+ """
+ settings = {
+ 'query': 'str'
+ }
+
+ def __init__( self, source, query=None, **kwargs ):
+ self.query = query
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataDictProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ for row in cur.execute(self.query):
+ yield [dict((cur.description[i][0], value) for i, value in enumerate(row))]
+ else:
+ yield
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
0
3 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/1e6035e53de5/
Changeset: 1e6035e53de5
User: jjohnson
Date: 2014-12-12 19:35:40+00:00
Summary: Add SQlite dataproviders: SQliteDataTableProvider - Allows any query to be run and returns the resulting rows as arrays of arrays, SQliteDataDictProvider - Allows any query to be run and returns the resulting rows as arrays of dicts
Affected #: 1 file
diff -r efd0286e7629289c4c583898ebecb32646834c64 -r 1e6035e53de58ce4fe1687ab74512da711d99989 lib/galaxy/datatypes/dataproviders/dataset.py
--- a/lib/galaxy/datatypes/dataproviders/dataset.py
+++ b/lib/galaxy/datatypes/dataproviders/dataset.py
@@ -13,6 +13,7 @@
import external
from galaxy.util import sqlite
import re
+import sys
from galaxy import eggs
eggs.require( 'bx-python' )
@@ -726,3 +727,55 @@
else:
yield
+class SQliteDataTableProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of arrays
+ """
+ settings = {
+ 'query': 'str',
+ 'headers': 'bool',
+ 'limit': 'int'
+ }
+
+ def __init__( self, source, query=None, headers=False, limit=sys.maxint, **kwargs ):
+ self.query = query
+ self.headers = headers
+ self.limit = limit
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataTableProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ results = cur.execute(self.query)
+ if self.headers:
+ yield [col[0] for col in cur.description]
+ for i,row in enumerate(results):
+ if i >= self.limit:
+ break
+ yield [val for val in row]
+ else:
+ yield
+
+class SQliteDataDictProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of dicts
+ """
+ settings = {
+ 'query': 'str'
+ }
+
+ def __init__( self, source, query=None, **kwargs ):
+ self.query = query
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataDictProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ for row in cur.execute(self.query):
+ yield [dict((cur.description[i][0], value) for i, value in enumerate(row))]
+ else:
+ yield
https://bitbucket.org/galaxy/galaxy-central/commits/c207054ea196/
Changeset: c207054ea196
User: jjohnson
Date: 2014-12-12 19:41:35+00:00
Summary: Add dataproviders to the SQlite dataype
Affected #: 1 file
diff -r 1e6035e53de58ce4fe1687ab74512da711d99989 -r c207054ea1966bc9d4640a6215192414e8071739 lib/galaxy/datatypes/binary.py
--- a/lib/galaxy/datatypes/binary.py
+++ b/lib/galaxy/datatypes/binary.py
@@ -620,6 +620,16 @@
dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
return dataproviders.dataset.SQliteDataProvider( dataset_source, **settings )
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-table', dataproviders.dataset.SQliteDataTableProvider.settings )
+ def sqlite_datatableprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataTableProvider( dataset_source, **settings )
+
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-dict', dataproviders.dataset.SQliteDataDictProvider.settings )
+ def sqlite_datadictprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataDictProvider( dataset_source, **settings )
+
Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
https://bitbucket.org/galaxy/galaxy-central/commits/ad28e42a8af3/
Changeset: ad28e42a8af3
User: dannon
Date: 2014-12-15 12:43:42+00:00
Summary: Merged in jjohnson/galaxy-central-7 (pull request #609)
Add dataproviders for SQlite datatype
Affected #: 2 files
diff -r ecac8a8679612d419347dcbedc398319418b077e -r ad28e42a8af34c81f0ebcbe6f3baef9bdcf80ae8 lib/galaxy/datatypes/binary.py
--- a/lib/galaxy/datatypes/binary.py
+++ b/lib/galaxy/datatypes/binary.py
@@ -620,6 +620,16 @@
dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
return dataproviders.dataset.SQliteDataProvider( dataset_source, **settings )
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-table', dataproviders.dataset.SQliteDataTableProvider.settings )
+ def sqlite_datatableprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataTableProvider( dataset_source, **settings )
+
+ @dataproviders.decorators.dataprovider_factory( 'sqlite-dict', dataproviders.dataset.SQliteDataDictProvider.settings )
+ def sqlite_datadictprovider( self, dataset, **settings ):
+ dataset_source = dataproviders.dataset.DatasetDataProvider( dataset )
+ return dataproviders.dataset.SQliteDataDictProvider( dataset_source, **settings )
+
Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
diff -r ecac8a8679612d419347dcbedc398319418b077e -r ad28e42a8af34c81f0ebcbe6f3baef9bdcf80ae8 lib/galaxy/datatypes/dataproviders/dataset.py
--- a/lib/galaxy/datatypes/dataproviders/dataset.py
+++ b/lib/galaxy/datatypes/dataproviders/dataset.py
@@ -13,6 +13,7 @@
import external
from galaxy.util import sqlite
import re
+import sys
from galaxy import eggs
eggs.require( 'bx-python' )
@@ -726,3 +727,55 @@
else:
yield
+class SQliteDataTableProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of arrays
+ """
+ settings = {
+ 'query': 'str',
+ 'headers': 'bool',
+ 'limit': 'int'
+ }
+
+ def __init__( self, source, query=None, headers=False, limit=sys.maxint, **kwargs ):
+ self.query = query
+ self.headers = headers
+ self.limit = limit
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataTableProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ results = cur.execute(self.query)
+ if self.headers:
+ yield [col[0] for col in cur.description]
+ for i,row in enumerate(results):
+ if i >= self.limit:
+ break
+ yield [val for val in row]
+ else:
+ yield
+
+class SQliteDataDictProvider( base.DataProvider ):
+ """
+ Data provider that uses a sqlite database file as its source.
+ Allows any query to be run and returns the resulting rows as arrays of dicts
+ """
+ settings = {
+ 'query': 'str'
+ }
+
+ def __init__( self, source, query=None, **kwargs ):
+ self.query = query
+ self.connection = sqlite.connect(source.dataset.file_name)
+ super( SQliteDataDictProvider, self ).__init__( source, **kwargs )
+
+ def __iter__( self ):
+ if (self.query is not None) and sqlite.is_read_only_query(self.query):
+ cur = self.connection.cursor()
+ for row in cur.execute(self.query):
+ yield [dict((cur.description[i][0], value) for i, value in enumerate(row))]
+ else:
+ yield
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
0
7 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/563a28409672/
Changeset: 563a28409672
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Allow multiple tool data table config files...
... use this functionality to implement tests for tool data API.
Affected #: 5 files
diff -r 6363c839528ca8bbbbc60c1b3784c788468d9445 -r 563a28409672988f6b30545ba987017e7da2e14c lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -31,8 +31,10 @@
# at server startup. If tool shed repositories are installed that contain a valid file named tool_data_table_conf.xml.sample, entries
# from that file are inserted into this dict at the time of installation.
self.data_tables = {}
- if config_filename:
- self.load_from_config_file( config_filename, self.tool_data_path, from_shed_config=False )
+ for single_config_filename in util.listify( config_filename ):
+ if not single_config_filename:
+ continue
+ self.load_from_config_file( single_config_filename, self.tool_data_path, from_shed_config=False )
def __getitem__( self, key ):
return self.data_tables.__getitem__( key )
diff -r 6363c839528ca8bbbbc60c1b3784c788468d9445 -r 563a28409672988f6b30545ba987017e7da2e14c scripts/functional_tests.py
--- a/scripts/functional_tests.py
+++ b/scripts/functional_tests.py
@@ -231,7 +231,16 @@
start_server = 'GALAXY_TEST_EXTERNAL' not in os.environ
tool_data_table_config_path = None
if os.path.exists( 'tool_data_table_conf.test.xml' ):
+ # If explicitly defined tables for test, use those.
tool_data_table_config_path = 'tool_data_table_conf.test.xml'
+ else:
+ # ... otherise find whatever Galaxy would use as the default and
+ # the sample data for fucntional tests to that.
+ default_tool_data_config = 'config/tool_data_table_conf.xml.sample'
+ for tool_data_config in ['config/tool_data_table_conf.xml', 'tool_data_table_conf.xml' ]:
+ if os.path.exists( tool_data_config ):
+ default_tool_data_config = tool_data_config
+ tool_data_table_config_path = '%s,test/functional/tool-data/sample_tool_data_tables.xml' % default_tool_data_config
shed_tool_data_table_config = 'config/shed_tool_data_table_conf.xml'
tool_dependency_dir = os.environ.get( 'GALAXY_TOOL_DEPENDENCY_DIR', None )
use_distributed_object_store = os.environ.get( 'GALAXY_USE_DISTRIBUTED_OBJECT_STORE', False )
diff -r 6363c839528ca8bbbbc60c1b3784c788468d9445 -r 563a28409672988f6b30545ba987017e7da2e14c test/api/test_tool_data.py
--- /dev/null
+++ b/test/api/test_tool_data.py
@@ -0,0 +1,30 @@
+""" Tests for the tool data API.
+"""
+from base import api
+
+import operator
+
+
+class ToolDataApiTestCase( api.ApiTestCase ):
+
+ def test_admin_only( self ):
+ index_response = self._get( "tool_data", admin=False )
+ self._assert_status_code_is( index_response, 403 )
+
+ def test_list(self):
+ index_response = self._get( "tool_data", admin=True )
+ self._assert_status_code_is( index_response, 200 )
+ print index_response.content
+ index = index_response.json()
+ assert "testalpha" in map(operator.itemgetter("name"), index)
+
+ def test_show(self):
+ show_response = self._get( "tool_data/testalpha", admin=True )
+ self._assert_status_code_is( show_response, 200 )
+ print show_response.content
+ data_table = show_response.json()
+ assert data_table["columns"] == ["value", "name", "path"]
+ first_entry = data_table["fields"][0]
+ assert first_entry[0] == "data1"
+ assert first_entry[1] == "data1name"
+ assert first_entry[2].endswith("test/functional/tool-data/data1/entry.txt")
diff -r 6363c839528ca8bbbbc60c1b3784c788468d9445 -r 563a28409672988f6b30545ba987017e7da2e14c test/functional/tool-data/sample_tool_data_tables.xml
--- /dev/null
+++ b/test/functional/tool-data/sample_tool_data_tables.xml
@@ -0,0 +1,12 @@
+<tables>
+ <table name="testalpha" comment_char="#">
+ <columns>value, name, path</columns>
+ <file path="${__HERE__}/testalpha.loc" />
+ </table>
+ <!--
+ <table name="testbeta" comment_char="#">
+ <columns>value, foo, bar, path</columns>
+ <file path="${__HERE__}/testbeta.loc" />
+ </table>
+ -->
+</tables>
diff -r 6363c839528ca8bbbbc60c1b3784c788468d9445 -r 563a28409672988f6b30545ba987017e7da2e14c test/functional/tool-data/testalpha.loc
--- /dev/null
+++ b/test/functional/tool-data/testalpha.loc
@@ -0,0 +1,2 @@
+data1 data1name ${__HERE__}/data1/entry.txt
+data2 data2name ${__HERE__}/data2/entry.txt
https://bitbucket.org/galaxy/galaxy-central/commits/8795d0fe13c4/
Changeset: 8795d0fe13c4
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: PEP-8 fixes for lib/galaxy/tools/data/__init__.py.
Affected #: 1 file
diff -r 563a28409672988f6b30545ba987017e7da2e14c -r 8795d0fe13c45804ea53f4543c71ee7ca6f41a0c lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -9,9 +9,7 @@
import logging
import os
import os.path
-import shutil
import string
-import tempfile
from galaxy import util
from galaxy.util.odict import odict
@@ -22,6 +20,7 @@
DEFAULT_TABLE_TYPE = 'tabular'
+
class ToolDataTableManager( object ):
"""Manages a collection of tool data tables"""
@@ -81,7 +80,7 @@
log.debug( "Loaded tool data table '%s'", table.name )
else:
log.debug( "Loading another instance of data table '%s', attempting to merge content.", table.name )
- self.data_tables[ table.name ].merge_tool_data_table( table, allow_duplicates=False ) #only merge content, do not persist to disk, do not allow duplicate rows when merging
+ self.data_tables[ table.name ].merge_tool_data_table( table, allow_duplicates=False ) # only merge content, do not persist to disk, do not allow duplicate rows when merging
# FIXME: This does not account for an entry with the same unique build ID, but a different path.
return table_elems
@@ -128,7 +127,7 @@
"""
if not ( new_elems or remove_elems ):
log.debug( 'ToolDataTableManager.to_xml_file called without any elements to add or remove.' )
- return #no changes provided, no need to persist any changes
+ return # no changes provided, no need to persist any changes
if not new_elems:
new_elems = []
if not remove_elems:
@@ -175,7 +174,7 @@
assert table_type in tool_data_table_types, "Unknown data table type '%s'" % type
return tool_data_table_types[ table_type ]( table_elem, tool_data_path, from_shed_config=from_shed_config, filename=filename )
- def __init__( self, config_element, tool_data_path, from_shed_config = False, filename=None ):
+ def __init__( self, config_element, tool_data_path, from_shed_config=False, filename=None ):
self.name = config_element.get( 'name' )
self.comment_char = config_element.get( 'comment_char' )
self.empty_field_value = config_element.get( 'empty_field_value', '' )
@@ -187,7 +186,7 @@
# increment this variable any time a new entry is added, or when the table is totally reloaded
# This value has no external meaning, and does not represent an abstract version of the underlying data
self._loaded_content_version = 1
- self._load_info = ( [ config_element, tool_data_path ], { 'from_shed_config':from_shed_config } )
+ self._load_info = ( [ config_element, tool_data_path ], { 'from_shed_config': from_shed_config } )
self._merged_load_info = []
def _update_version( self, version=None ):
@@ -252,13 +251,13 @@
type_key = 'tabular'
- def __init__( self, config_element, tool_data_path, from_shed_config = False, filename=None ):
+ def __init__( self, config_element, tool_data_path, from_shed_config=False, filename=None ):
super( TabularToolDataTable, self ).__init__( config_element, tool_data_path, from_shed_config, filename)
self.config_element = config_element
self.data = []
self.configure_and_load( config_element, tool_data_path, from_shed_config)
- def configure_and_load( self, config_element, tool_data_path, from_shed_config = False):
+ def configure_and_load( self, config_element, tool_data_path, from_shed_config=False):
"""
Configure and load table from an XML element.
"""
@@ -323,7 +322,6 @@
else:
log.debug( "Filename '%s' already exists in filenames (%s), not adding", filename, self.filenames.keys() )
-
def merge_tool_data_table( self, other_table, allow_duplicates=True, persist=False, persist_on_error=False, entry_source=None, **kwd ):
assert self.columns == other_table.columns, "Merging tabular data tables with non matching columns is not allowed: %s:%s != %s:%s" % ( self.name, self.columns, other_table.name, other_table.columns )
#merge filename info
@@ -350,7 +348,7 @@
for i, field in enumerate( fields ):
field_name = named_colums[i]
if field_name is None:
- field_name = i #check that this is supposed to be 0 based.
+ field_name = i # check that this is supposed to be 0 based.
field_dict[ field_name ] = field
rval.append( field_dict )
return rval
@@ -508,7 +506,7 @@
data_table_fh = open( filename, 'wb' )
if os.stat( filename )[6] != 0:
# ensure last existing line ends with new line
- data_table_fh.seek( -1, 2 ) #last char in file
+ data_table_fh.seek( -1, 2 ) # last char in file
last_char = data_table_fh.read( 1 )
if last_char not in [ '\n', '\r' ]:
data_table_fh.write( '\n' )
@@ -577,7 +575,7 @@
def to_dict(self, view='collection'):
rval = super(TabularToolDataTable, self).to_dict()
if view == 'element':
- rval['columns'] = sorted(self.columns.keys(), key=lambda x:self.columns[x])
+ rval['columns'] = sorted(self.columns.keys(), key=lambda x: self.columns[x])
rval['fields'] = self.get_fields()
return rval
https://bitbucket.org/galaxy/galaxy-central/commits/6d54a7fd1fe9/
Changeset: 6d54a7fd1fe9
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Small improvements to tool data API.
Update tool data API to use newer API decorator for better error handling, create some abstractions for reuse down stream, etc...
Affected #: 1 file
diff -r 8795d0fe13c45804ea53f4543c71ee7ca6f41a0c -r 6d54a7fd1fe9edfdf2f5c2fdb1b5ddb7eebdf59a lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -1,4 +1,6 @@
+from galaxy import exceptions
from galaxy import web
+from galaxy.web import _future_expose_api as expose_api
from galaxy.web.base.controller import BaseAPIController
@@ -8,21 +10,21 @@
"""
@web.require_admin
- @web.expose_api
+ @expose_api
def index( self, trans, **kwds ):
"""
GET /api/tool_data: returns a list tool_data tables::
"""
- return list( a.to_dict() for a in trans.app.tool_data_tables.data_tables.values() )
+ return list( a.to_dict() for a in self._data_tables.values() )
@web.require_admin
- @web.expose_api
+ @expose_api
def show( self, trans, id, **kwds ):
- return trans.app.tool_data_tables.data_tables[id].to_dict(view='element')
+ return self._data_table(id).to_dict(view='element')
@web.require_admin
- @web.expose_api
+ @expose_api
def delete( self, trans, id, **kwd ):
"""
DELETE /api/tool_data/{id}
@@ -61,3 +63,13 @@
return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list())))
return data_table.remove_entry(split_values)
+
+ def _data_table( self, id ):
+ try:
+ return self._data_tables[id]
+ except IndexError:
+ raise exceptions.ObjectNotFound("No such data table %s" % id)
+
+ @property
+ def _data_tables( self ):
+ return self.app.tool_data_tables.data_tables
https://bitbucket.org/galaxy/galaxy-central/commits/edbe286564de/
Changeset: edbe286564de
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Implement a detailed break down of data table fields via API.
Originally this approach was laid out by Kyle Ellrott in this pull request (https://bitbucket.org/galaxy/galaxy-central/pull-request/531/add-downloads-…) The changes to galaxy.tools.data are entirely his contribution, I only reworked the API and endpoint slightly and did some stylistic fixes and refactoring.
Affected #: 4 files
diff -r 6d54a7fd1fe9edfdf2f5c2fdb1b5ddb7eebdf59a -r edbe286564de14b2d51656c03443b3e4aa99e3ab lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -9,7 +9,11 @@
import logging
import os
import os.path
+import re
import string
+import hashlib
+
+from glob import glob
from galaxy import util
from galaxy.util.odict import odict
@@ -340,6 +344,13 @@
def get_fields( self ):
return self.data
+ def get_field(self, value):
+ rval = None
+ for i in self.get_named_fields_list():
+ if i['value'] == value:
+ rval = TabularToolDataField(i)
+ return rval
+
def get_named_fields_list( self ):
rval = []
named_colums = self.get_column_name_list()
@@ -580,6 +591,58 @@
return rval
+class TabularToolDataField(Dictifiable, object):
+
+ dict_collection_visible_keys = []
+
+ def __init__(self, data):
+ self.data = data
+
+ def __getitem__(self, key):
+ return self.data[key]
+
+ def get_base_path(self):
+ return os.path.normpath(os.path.abspath( self.data['path'] ))
+
+ def get_base_dir(self):
+ path = self.get_base_path()
+ if not os.path.isdir(path):
+ path = os.path.dirname(path)
+ return path
+
+ def clean_base_dir(self, path):
+ return re.sub( "^" + self.get_base_dir() + r"/*", "", path )
+
+ def get_files(self):
+ return glob( self.get_base_path() + "*" )
+
+ def get_filesize_map(self, rm_base_dir=False):
+ out = {}
+ for path in self.get_files():
+ if rm_base_dir:
+ out[self.clean_base_dir(path)] = os.path.getsize(path)
+ else:
+ out[path] = os.path.getsize(path)
+ return out
+
+ def get_fingerprint(self):
+ sha1 = hashlib.sha1()
+ fmap = self.get_filesize_map(True)
+ for k in sorted(fmap.keys()):
+ sha1.update(k)
+ sha1.update(str(fmap[k]))
+ return sha1.hexdigest()
+
+ def to_dict(self):
+ rval = super(TabularToolDataField, self).to_dict()
+ rval['name'] = self.data['value']
+ rval['fields'] = self.data
+ rval['base_dir'] = self.get_base_dir(),
+ rval['files'] = self.get_filesize_map(True)
+ rval['fingerprint'] = self.get_fingerprint()
+ return rval
+
+
def expand_here_template(content, here=None):
if here and content:
content = string.Template(content).safe_substitute( { "__HERE__": here })
diff -r 6d54a7fd1fe9edfdf2f5c2fdb1b5ddb7eebdf59a -r edbe286564de14b2d51656c03443b3e4aa99e3ab lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -64,6 +64,22 @@
return data_table.remove_entry(split_values)
+ @web.require_admin
+ @expose_api
+ def show_field( self, trans, id, value, **kwds ):
+ """
+ GET /api/tool_data/<id>/fields/<value>
+
+ Get information about a partiular field in a tool_data table
+ """
+ return self._data_table_field( id, value ).to_dict()
+
+ def _data_table_field( self, id, value ):
+ out = self._data_table(id).get_field(value)
+ if out is None:
+ raise exceptions.ObjectNotFound("No such field %s in data table %s." % (value, id))
+ return out
+
def _data_table( self, id ):
try:
return self._data_tables[id]
diff -r 6d54a7fd1fe9edfdf2f5c2fdb1b5ddb7eebdf59a -r edbe286564de14b2d51656c03443b3e4aa99e3ab lib/galaxy/webapps/galaxy/buildapp.py
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -171,6 +171,7 @@
webapp.mapper.resource( 'dataset', 'datasets', path_prefix='/api' )
webapp.mapper.resource( 'tool_data', 'tool_data', path_prefix='/api' )
+ webapp.mapper.connect( '/api/tool_data/{id:.+?}/fields/{value:.+?}', action='show_field', controller="tool_data" )
webapp.mapper.resource( 'dataset_collection', 'dataset_collections', path_prefix='/api/')
webapp.mapper.resource( 'sample', 'samples', path_prefix='/api' )
webapp.mapper.resource( 'request', 'requests', path_prefix='/api' )
diff -r 6d54a7fd1fe9edfdf2f5c2fdb1b5ddb7eebdf59a -r edbe286564de14b2d51656c03443b3e4aa99e3ab test/api/test_tool_data.py
--- a/test/api/test_tool_data.py
+++ b/test/api/test_tool_data.py
@@ -28,3 +28,11 @@
assert first_entry[0] == "data1"
assert first_entry[1] == "data1name"
assert first_entry[2].endswith("test/functional/tool-data/data1/entry.txt")
+
+ def test_show_field(self):
+ show_field_response = self._get( "tool_data/testalpha/fields/data1", admin=True )
+ self._assert_status_code_is( show_field_response, 200 )
+ field = show_field_response.json()
+ self._assert_has_keys( field, "files", "name", "fields", "fingerprint", "base_dir" )
+ files = field[ "files" ]
+ assert len( files ) == 2
https://bitbucket.org/galaxy/galaxy-central/commits/e416c3a2f956/
Changeset: e416c3a2f956
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Allow downloading index files via tool data API.
This provides direct access to the files to admins - probably still wise to provide some mechanism to download a copressed archive of these files.
Affected #: 3 files
diff -r edbe286564de14b2d51656c03443b3e4aa99e3ab -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -1,6 +1,9 @@
+import os
+
from galaxy import exceptions
from galaxy import web
from galaxy.web import _future_expose_api as expose_api
+from galaxy.web import _future_expose_api_raw as expose_api_raw
from galaxy.web.base.controller import BaseAPIController
@@ -74,6 +77,16 @@
"""
return self._data_table_field( id, value ).to_dict()
+ @web.require_admin
+ @expose_api_raw
+ def download_field_file( self, trans, id, value, path, **kwds ):
+ field_value = self._data_table_field( id, value )
+ base_dir = field_value.get_base_dir()
+ full_path = os.path.join( base_dir, path )
+ if full_path not in field_value.get_files():
+ raise exceptions.ObjectNotFound("No such path in data table field.")
+ return open(full_path, "r")
+
def _data_table_field( self, id, value ):
out = self._data_table(id).get_field(value)
if out is None:
diff -r edbe286564de14b2d51656c03443b3e4aa99e3ab -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b lib/galaxy/webapps/galaxy/buildapp.py
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -171,6 +171,7 @@
webapp.mapper.resource( 'dataset', 'datasets', path_prefix='/api' )
webapp.mapper.resource( 'tool_data', 'tool_data', path_prefix='/api' )
+ webapp.mapper.connect( '/api/tool_data/{id:.+?}/fields/{value:.+?}/files/{path:.+?}', action='download_field_file', controller="tool_data" )
webapp.mapper.connect( '/api/tool_data/{id:.+?}/fields/{value:.+?}', action='show_field', controller="tool_data" )
webapp.mapper.resource( 'dataset_collection', 'dataset_collections', path_prefix='/api/')
webapp.mapper.resource( 'sample', 'samples', path_prefix='/api' )
diff -r edbe286564de14b2d51656c03443b3e4aa99e3ab -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b test/api/test_tool_data.py
--- a/test/api/test_tool_data.py
+++ b/test/api/test_tool_data.py
@@ -36,3 +36,9 @@
self._assert_has_keys( field, "files", "name", "fields", "fingerprint", "base_dir" )
files = field[ "files" ]
assert len( files ) == 2
+
+ def test_download_field_file(self):
+ show_field_response = self._get( "tool_data/testalpha/fields/data1/files/entry.txt", admin=True )
+ self._assert_status_code_is( show_field_response, 200 )
+ content = show_field_response.content
+ assert content == "This is data 1.", content
https://bitbucket.org/galaxy/galaxy-central/commits/66fbbbd58209/
Changeset: 66fbbbd58209
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Allow specification of multiple data manager configuration files...
... use this to create API functional tests for data managers.
Affected #: 7 files
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 lib/galaxy/tools/data_manager/manager.py
--- a/lib/galaxy/tools/data_manager/manager.py
+++ b/lib/galaxy/tools/data_manager/manager.py
@@ -25,7 +25,10 @@
self.managed_data_tables = odict()
self.tool_path = None
self.filename = xml_filename or self.app.config.data_manager_config_file
- self.load_from_xml( self.filename )
+ for filename in util.listify( self.filename ):
+ if not filename:
+ continue
+ self.load_from_xml( filename )
if self.app.config.shed_data_manager_config_file:
self.load_from_xml( self.app.config.shed_data_manager_config_file, store_tool_path=False, replace_existing=True )
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 scripts/functional_tests.py
--- a/scripts/functional_tests.py
+++ b/scripts/functional_tests.py
@@ -241,6 +241,12 @@
if os.path.exists( tool_data_config ):
default_tool_data_config = tool_data_config
tool_data_table_config_path = '%s,test/functional/tool-data/sample_tool_data_tables.xml' % default_tool_data_config
+
+ default_data_manager_config = 'config/data_manager_conf.xml.sample'
+ for data_manager_config in ['config/data_manager_conf.xml', 'data_manager_conf.xml' ]:
+ if os.path.exists( data_manager_config ):
+ default_data_manager_config = data_manager_config
+ data_manager_config_file = "%s,test/functional/tools/sample_data_manager_conf.xml" % default_data_manager_config
shed_tool_data_table_config = 'config/shed_tool_data_table_conf.xml'
tool_dependency_dir = os.environ.get( 'GALAXY_TOOL_DEPENDENCY_DIR', None )
use_distributed_object_store = os.environ.get( 'GALAXY_USE_DISTRIBUTED_OBJECT_STORE', False )
@@ -329,6 +335,7 @@
master_api_key=master_api_key,
use_tasked_jobs=True,
enable_beta_tool_formats=True,
+ data_manager_config_file=data_manager_config_file,
)
if install_database_connection is not None:
kwargs[ 'install_database_connection' ] = install_database_connection
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 test/api/test_tool_data.py
--- a/test/api/test_tool_data.py
+++ b/test/api/test_tool_data.py
@@ -1,6 +1,9 @@
""" Tests for the tool data API.
"""
+import json
+
from base import api
+from .helpers import DatasetPopulator
import operator
@@ -42,3 +45,19 @@
self._assert_status_code_is( show_field_response, 200 )
content = show_field_response.content
assert content == "This is data 1.", content
+
+ def test_create_data_with_manager(self):
+ dataset_populator = DatasetPopulator( self.galaxy_interactor )
+ history_id = dataset_populator.new_history()
+ payload = dataset_populator.run_tool_payload(
+ tool_id="data_manager",
+ inputs={"ignored_value": "moo"},
+ history_id=history_id,
+ )
+ create_response = self._post( "tools", data=payload )
+ self._assert_status_code_is( create_response, 200 )
+ dataset_populator.wait_for_history( history_id, assert_ok=True )
+ show_response = self._get( "tool_data/testbeta", admin=True )
+ print show_response.content
+ assert False
+
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 test/functional/tool-data/sample_tool_data_tables.xml
--- a/test/functional/tool-data/sample_tool_data_tables.xml
+++ b/test/functional/tool-data/sample_tool_data_tables.xml
@@ -3,10 +3,8 @@
<columns>value, name, path</columns><file path="${__HERE__}/testalpha.loc" /></table>
- <!--
<table name="testbeta" comment_char="#">
- <columns>value, foo, bar, path</columns>
+ <columns>value, path</columns><file path="${__HERE__}/testbeta.loc" /></table>
- --></tables>
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 test/functional/tools/data_manager.xml
--- /dev/null
+++ b/test/functional/tools/data_manager.xml
@@ -0,0 +1,16 @@
+<tool id="data_manager" name="Test Data Manager" tool_type="manage_data" version="0.0.1">
+ <configfiles>
+ <configfile name="static_test_data">{"data_tables": {"testbeta": [{"value": "newvalue", "path": "newvalue.txt"}]}}</configfile>
+ </configfiles>
+ <command>
+ mkdir $out_file.files_path ;
+ echo "A new value" > $out_file.files_path/newvalue.txt;
+ cp $static_test_data $out_file
+ </command>
+ <inputs>
+ <param type="text" name="ignored_value" value="" label="Ignored" />
+ </inputs>
+ <outputs>
+ <data name="out_file" format="data_manager_json"/>
+ </outputs>
+</tool>
diff -r e416c3a2f956a28127eb730076f37ad6ee4b1d8b -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 test/functional/tools/sample_data_manager_conf.xml
--- /dev/null
+++ b/test/functional/tools/sample_data_manager_conf.xml
@@ -0,0 +1,16 @@
+<data_managers tool_path="test/functional/tools">
+ <data_manager tool_file="data_manager.xml" id="test_data_manager" version="1.0">
+ <data_table name="testbeta">
+ <output>
+ <column name="value" />
+ <column name="path" output_ref="out_file" >
+ <move type="directory" relativize_symlinks="True">
+ <target base="${GALAXY_DATA_MANAGER_DATA_PATH}">testbeta/${value}</target>
+ </move>
+ <value_translation>${GALAXY_DATA_MANAGER_DATA_PATH}/testbeta/${value}/${path}</value_translation>
+ <value_translation type="function">abspath</value_translation>
+ </column>
+ </output>
+ </data_table>
+ </data_manager>
+</data_managers>
https://bitbucket.org/galaxy/galaxy-central/commits/ecac8a867961/
Changeset: ecac8a867961
User: jmchilton
Date: 2014-12-15 05:20:35+00:00
Summary: Implement test case for Pull Request #577.
Affected #: 1 file
diff -r 66fbbbd58209dba224f95c2aaf973ec7c64b7c11 -r ecac8a8679612d419347dcbedc398319418b077e test/api/test_tool_data.py
--- a/test/api/test_tool_data.py
+++ b/test/api/test_tool_data.py
@@ -5,6 +5,8 @@
from base import api
from .helpers import DatasetPopulator
+from requests import delete
+
import operator
@@ -46,7 +48,10 @@
content = show_field_response.content
assert content == "This is data 1.", content
- def test_create_data_with_manager(self):
+ def test_delete_entry(self):
+ show_response = self._get( "tool_data/testbeta", admin=True )
+ original_count = len(show_response.json()["fields"])
+
dataset_populator = DatasetPopulator( self.galaxy_interactor )
history_id = dataset_populator.new_history()
payload = dataset_populator.run_tool_payload(
@@ -58,6 +63,12 @@
self._assert_status_code_is( create_response, 200 )
dataset_populator.wait_for_history( history_id, assert_ok=True )
show_response = self._get( "tool_data/testbeta", admin=True )
- print show_response.content
- assert False
+ updated_fields = show_response.json()["fields"]
+ assert len(updated_fields) == original_count + 1
+ field0 = updated_fields[0]
+ url = self._api_url( "tool_data/testbeta?key=%s" % self.galaxy_interactor.master_api_key )
+ delete( url, data=json.dumps({"values": "\t".join(field0)}) )
+ show_response = self._get( "tool_data/testbeta", admin=True )
+ updated_fields = show_response.json()["fields"]
+ assert len(updated_fields) == original_count
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
0
commit/galaxy-central: jmchilton: Merged in abretaud/galaxy-central (pull request #577)
by commits-noreply@bitbucket.org 15 Dec '14
by commits-noreply@bitbucket.org 15 Dec '14
15 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/6363c839528c/
Changeset: 6363c839528c
User: jmchilton
Date: 2014-12-15 05:17:31+00:00
Summary: Merged in abretaud/galaxy-central (pull request #577)
Add an API to remove items from tool data tables
Affected #: 2 files
diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -211,6 +211,13 @@
self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd )
return self._loaded_content_version
+ def _remove_entry(self, values):
+ raise NotImplementedError( "Abstract method" )
+
+ def remove_entry(self, values):
+ self._remove_entry( values )
+ return self._update_version()
+
def is_current_version( self, other_version ):
return self._loaded_content_version == other_version
@@ -506,6 +513,42 @@
data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) )
return not is_error
+ def _remove_entry( self, values):
+
+ # update every file
+ for filename in self.filenames:
+
+ if os.path.exists( filename ):
+ values = self._replace_field_separators( values )
+ self.filter_file_fields( filename, values )
+ else:
+ log.warn( "Cannot find index file '%s' for tool data table '%s'" % ( filename, self.name ) )
+
+ self.reload_from_files()
+
+ def filter_file_fields( self, loc_file, values ):
+ """
+ Reads separated lines from file and print back only the lines that pass a filter.
+ """
+ separator_char = (lambda c: '<TAB>' if c == '\t' else c)(self.separator)
+
+ with open(loc_file) as reader:
+ rval = ""
+ for i, line in enumerate( reader ):
+ if line.lstrip().startswith( self.comment_char ):
+ rval += line
+ else:
+ line_s = line.rstrip( "\n\r" )
+ if line_s:
+ fields = line_s.split( self.separator )
+ if fields != values:
+ rval += line
+
+ with open(loc_file, 'wb') as writer:
+ writer.write(rval)
+
+ return rval
+
def _replace_field_separators( self, fields, separator=None, replace=None, comment_char=None ):
#make sure none of the fields contain separator
#make sure separator replace is different from comment_char,
diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -20,3 +20,44 @@
@web.expose_api
def show( self, trans, id, **kwds ):
return trans.app.tool_data_tables.data_tables[id].to_dict(view='element')
+
+ @web.require_admin
+ @web.expose_api
+ def delete( self, trans, id, **kwd ):
+ """
+ DELETE /api/tool_data/{id}
+ Removes an item from a data table
+
+ :type id: str
+ :param id: the id of the data table containing the item to delete
+ :type kwd: dict
+ :param kwd: (required) dictionary structure containing:
+
+ * payload: a dictionary itself containing:
+ * values: <TAB> separated list of column contents, there must be a value for all the columns of the data table
+ """
+ decoded_tool_data_id = id
+
+ try:
+ data_table = trans.app.tool_data_tables.data_tables.get(decoded_tool_data_id)
+ except:
+ data_table = None
+ if not data_table:
+ trans.response.status = 400
+ return "Invalid data table id ( %s ) specified." % str( decoded_tool_data_id )
+
+ values = None
+ if kwd.get( 'payload', None ):
+ values = kwd['payload'].get( 'values', '' )
+
+ if not values:
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified." % str( values )
+
+ split_values = values.split("\t")
+
+ if len(split_values) != len(data_table.get_column_name_list()):
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list())))
+
+ return data_table.remove_entry(split_values)
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
0
4 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/5edaafeecfed/
Changeset: 5edaafeecfed
User: abretaud
Date: 2014-11-27 13:18:18+00:00
Summary: Add an API to remove items from tool data tables
Affected #: 2 files
diff -r 3975b317ffdcbaa6c8dcb4b2b52404bfa34edbc2 -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -211,6 +211,13 @@
self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd )
return self._loaded_content_version
+ def _remove_entry(self, values, **kwd):
+ raise NotImplementedError( "Abstract method" )
+
+ def remove_entry(self, values, **kwd):
+ self._remove_entry_and_reload( values,**kwd )
+ return self._update_version()
+
def is_current_version( self, other_version ):
return self._loaded_content_version == other_version
@@ -506,6 +513,42 @@
data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) )
return not is_error
+ def _remove_entry_and_reload( self, values):
+
+ # update every file
+ for filename in self.filenames:
+
+ if os.path.exists( filename ):
+ values = self._replace_field_separators( values )
+ self.filter_file_fields( filename, values )
+ else:
+ log.warn( "Cannot find index file '%s' for tool data table '%s'" % ( filename, self.name ) )
+
+ self.reload_from_files()
+
+ def filter_file_fields( self, loc_file, values ):
+ """
+ Reads separated lines from file and print back only the lines that pass a filter.
+ """
+ separator_char = (lambda c: '<TAB>' if c == '\t' else c)(self.separator)
+
+ with open(loc_file) as reader:
+ rval = ""
+ for i, line in enumerate( reader ):
+ if line.lstrip().startswith( self.comment_char ):
+ rval += line
+ else:
+ line_s = line.rstrip( "\n\r" )
+ if line_s:
+ fields = line_s.split( self.separator )
+ if fields != values:
+ rval += line
+
+ with open(loc_file, 'wb') as writer:
+ writer.write(rval)
+
+ return rval
+
def _replace_field_separators( self, fields, separator=None, replace=None, comment_char=None ):
#make sure none of the fields contain separator
#make sure separator replace is different from comment_char,
diff -r 3975b317ffdcbaa6c8dcb4b2b52404bfa34edbc2 -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -20,3 +20,44 @@
@web.expose_api
def show( self, trans, id, **kwds ):
return trans.app.tool_data_tables.data_tables[id].to_dict(view='element')
+
+ @web.require_admin
+ @web.expose_api
+ def delete( self, trans, id, **kwd ):
+ """
+ DELETE /api/tool_data/{id}
+ Removes a role from a group
+
+ :type id: str
+ :param id: the encoded id of the history to delete
+ :type kwd: dict
+ :param kwd: (required) dictionary structure containing:
+
+ * payload: a dictionary itself containing:
+ * values: <TAB> separated list of column contents, there must be a value for all the columns of the data table
+ """
+ decoded_tool_data_id = id
+
+ try:
+ data_table = trans.app.tool_data_tables.data_tables.get(decoded_tool_data_id)
+ except:
+ data_table = None
+ if not data_table:
+ trans.response.status = 400
+ return "Invalid data table id ( %s ) specified." % str( decoded_tool_data_id )
+
+ values = None
+ if kwd.get( 'payload', None ):
+ values = kwd['payload'].get( 'values', '' )
+
+ if not values:
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified." % str( values )
+
+ split_values = values.split("\t")
+
+ if len(split_values) != len(data_table.get_column_name_list()):
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list())))
+
+ return data_table.remove_entry(split_values)
https://bitbucket.org/galaxy/galaxy-central/commits/d76253bb22c4/
Changeset: d76253bb22c4
User: abretaud
Date: 2014-12-09 14:01:47+00:00
Summary: Merged galaxy/galaxy-central into default
Affected #: 184 files
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b .hgtags
--- a/.hgtags
+++ b/.hgtags
@@ -20,4 +20,4 @@
ca45b78adb4152fc6e7395514d46eba6b7d0b838 release_2014.08.11
548ab24667d6206780237bd807f7d857a484c461 latest_2014.08.11
2092948937ac30ef82f71463a235c66d34987088 release_2014.10.06
-acc8d1e2bc88530aa8d8651cf5f88649f6769304 latest_2014.10.06
+acb2548443ae42d39ef200d035ccc0481d6b930c latest_2014.10.06
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/galaxy.library.js
--- a/client/galaxy/scripts/galaxy.library.js
+++ b/client/galaxy/scripts/galaxy.library.js
@@ -36,7 +36,7 @@
initialize: function() {
this.routesHit = 0;
//keep count of number of routes handled by the application
- Backbone.history.on('route', function() { this.routesHit++; }, this);
+ Backbone.history.on( 'route', function() { this.routesHit++; }, this );
},
routes: {
@@ -45,6 +45,7 @@
"library/:library_id/permissions" : "library_permissions",
"folders/:folder_id/permissions" : "folder_permissions",
"folders/:id" : "folder_content",
+ "folders/:id/page/:show_page" : "folder_page",
"folders/:folder_id/datasets/:dataset_id" : "dataset_detail",
"folders/:folder_id/datasets/:dataset_id/permissions" : "dataset_permissions",
"folders/:folder_id/datasets/:dataset_id/versions/:ldda_id" : "dataset_version",
@@ -53,13 +54,13 @@
},
back: function() {
- if(this.routesHit > 1) {
+ if( this.routesHit > 1 ) {
//more than one route hit -> user did not land to current page directly
window.history.back();
} else {
//otherwise go to the home page. Use replaceState if available so
//the navigation doesn't create an extra history entry
- this.navigate('#', {trigger:true, replace:true});
+ this.navigate( '#', { trigger:true, replace:true } );
}
}
});
@@ -71,7 +72,8 @@
with_deleted : false,
sort_order : 'asc',
sort_by : 'name',
- library_page_size : 20
+ library_page_size : 20,
+ folder_page_size : 15
}
});
@@ -94,10 +96,10 @@
this.library_router = new LibraryRouter();
- this.library_router.on('route:libraries', function() {
- Galaxy.libraries.libraryToolbarView = new mod_librarytoolbar_view.LibraryToolbarView();
- Galaxy.libraries.libraryListView = new mod_librarylist_view.LibraryListView();
- });
+ this.library_router.on( 'route:libraries', function() {
+ Galaxy.libraries.libraryToolbarView = new mod_librarytoolbar_view.LibraryToolbarView();
+ Galaxy.libraries.libraryListView = new mod_librarylist_view.LibraryListView();
+ });
this.library_router.on('route:libraries_page', function( show_page ) {
if ( Galaxy.libraries.libraryToolbarView === null ){
@@ -108,66 +110,77 @@
}
});
- this.library_router.on('route:folder_content', function(id) {
+ this.library_router.on( 'route:folder_content', function( id ) {
if (Galaxy.libraries.folderToolbarView){
- Galaxy.libraries.folderToolbarView.$el.unbind('click');
- }
- Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView({id: id});
- Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView({id: id});
- });
+ Galaxy.libraries.folderToolbarView.$el.unbind( 'click' );
+ }
+ Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( { id: id } );
+ Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: id } );
+ });
- this.library_router.on('route:download', function(folder_id, format) {
- if ($('#folder_list_body').find(':checked').length === 0) {
- mod_toastr.info( 'You must select at least one dataset to download' );
- Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: true, replace: true});
- } else {
- Galaxy.libraries.folderToolbarView.download(folder_id, format);
- Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: false, replace: true});
- }
- });
+ this.library_router.on( 'route:folder_page', function( id, show_page ) {
+ if ( Galaxy.libraries.folderToolbarView === null ){
+ Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( {id: id} );
+ Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: id, show_page: show_page } );
+ } else {
+ Galaxy.libraries.folderListView.render( { id: id, show_page: parseInt( show_page ) } )
+ }
+ });
- this.library_router.on('route:dataset_detail', function(folder_id, dataset_id){
- if (Galaxy.libraries.datasetView){
- Galaxy.libraries.datasetView.$el.unbind('click');
- }
- Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id});
- });
- this.library_router.on('route:dataset_version', function(folder_id, dataset_id, ldda_id){
- if (Galaxy.libraries.datasetView){
- Galaxy.libraries.datasetView.$el.unbind('click');
- }
- Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, ldda_id: ldda_id, show_version: true});
- });
+ this.library_router.on( 'route:download', function( folder_id, format ) {
+ if ( $( '#folder_list_body' ).find( ':checked' ).length === 0 ) {
+ mod_toastr.info( 'You must select at least one dataset to download' );
+ Galaxy.libraries.library_router.navigate( 'folders/' + folder_id, { trigger: true, replace: true } );
+ } else {
+ Galaxy.libraries.folderToolbarView.download( folder_id, format );
+ Galaxy.libraries.library_router.navigate( 'folders/' + folder_id, { trigger: false, replace: true } );
+ }
+ });
- this.library_router.on('route:dataset_permissions', function(folder_id, dataset_id){
- if (Galaxy.libraries.datasetView){
- Galaxy.libraries.datasetView.$el.unbind('click');
- }
- Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, show_permissions: true});
- });
+ this.library_router.on( 'route:dataset_detail', function(folder_id, dataset_id){
+ if (Galaxy.libraries.datasetView){
+ Galaxy.libraries.datasetView.$el.unbind('click');
+ }
+ Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id});
+ });
- this.library_router.on('route:library_permissions', function(library_id){
- if (Galaxy.libraries.libraryView){
- Galaxy.libraries.libraryView.$el.unbind('click');
- }
- Galaxy.libraries.libraryView = new mod_library_library_view.LibraryView({id: library_id, show_permissions: true});
- });
+ this.library_router.on( 'route:dataset_version', function(folder_id, dataset_id, ldda_id){
+ if (Galaxy.libraries.datasetView){
+ Galaxy.libraries.datasetView.$el.unbind('click');
+ }
+ Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, ldda_id: ldda_id, show_version: true});
+ });
- this.library_router.on('route:folder_permissions', function(folder_id){
- if (Galaxy.libraries.folderView){
- Galaxy.libraries.folderView.$el.unbind('click');
- }
- Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({id: folder_id, show_permissions: true});
- });
- this.library_router.on('route:import_datasets', function(folder_id, source){
- if (Galaxy.libraries.folderToolbarView && Galaxy.libraries.folderListView){
- Galaxy.libraries.folderToolbarView.showImportModal({source:source});
- } else {
- Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView({id: folder_id});
- Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView({id: folder_id});
- Galaxy.libraries.folderToolbarView.showImportModal({source: source});
- }
- });
+ this.library_router.on( 'route:dataset_permissions', function(folder_id, dataset_id){
+ if (Galaxy.libraries.datasetView){
+ Galaxy.libraries.datasetView.$el.unbind('click');
+ }
+ Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, show_permissions: true});
+ });
+
+ this.library_router.on( 'route:library_permissions', function(library_id){
+ if (Galaxy.libraries.libraryView){
+ Galaxy.libraries.libraryView.$el.unbind('click');
+ }
+ Galaxy.libraries.libraryView = new mod_library_library_view.LibraryView({id: library_id, show_permissions: true});
+ });
+
+ this.library_router.on( 'route:folder_permissions', function(folder_id){
+ if (Galaxy.libraries.folderView){
+ Galaxy.libraries.folderView.$el.unbind('click');
+ }
+ Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({id: folder_id, show_permissions: true});
+ });
+
+ this.library_router.on( 'route:import_datasets', function( folder_id, source ){
+ if ( Galaxy.libraries.folderToolbarView && Galaxy.libraries.folderListView ){
+ Galaxy.libraries.folderToolbarView.showImportModal( { source:source } );
+ } else {
+ Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( { id: folder_id } );
+ Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: folder_id } );
+ Galaxy.libraries.folderToolbarView.showImportModal( { source: source } );
+ }
+ });
Backbone.history.start({pushState: false});
}
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/filter-control.js
--- /dev/null
+++ b/client/galaxy/scripts/jq-plugins/ui/filter-control.js
@@ -0,0 +1,204 @@
+// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js
+// Uses AMD or browser globals to create a jQuery plugin.
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+
+}(function ($) {
+ //==============================================================================
+ /**
+ * Template function that produces a bootstrap dropdown to replace the
+ * vanilla HTML select input. Pass in an array of options and an initial selection:
+ * $( '.my-div' ).append( dropDownSelect( [ 'option1', 'option2' ], 'option2' );
+ *
+ * When the user changes the selected option a 'change.dropdown-select' event will
+ * fire with both the jq event and the new selection text as arguments.
+ *
+ * Get the currently selected choice using:
+ * var userChoice = $( '.my-div .dropdown-select .dropdown-select-selected' ).text();
+ *
+ */
+ function dropDownSelect( options, selected ){
+ // replacement for vanilla select element using bootstrap dropdowns instead
+ selected = selected || (( !_.isEmpty( options ) )?( options[0] ):( '' ));
+ var $select = $([
+ '<div class="dropdown-select btn-group">',
+ '<button type="button" class="btn btn-default">',
+ '<span class="dropdown-select-selected">' + selected + '</span>',
+ '</button>',
+ '</div>'
+ ].join( '\n' ));
+
+ // if there's only one option, do not style/create as buttons, dropdown - use simple span
+ // otherwise, a dropdown displaying the current selection
+ if( options && options.length > 1 ){
+ $select.find( 'button' )
+ .addClass( 'dropdown-toggle' ).attr( 'data-toggle', 'dropdown' )
+ .append( ' <span class="caret"></span>' );
+ $select.append([
+ '<ul class="dropdown-menu" role="menu">',
+ _.map( options, function( option ){
+ return [
+ '<li><a href="javascript:void(0)">', option, '</a></li>'
+ ].join( '' );
+ }).join( '\n' ),
+ '</ul>'
+ ].join( '\n' ));
+ }
+
+ // trigger 'change.dropdown-select' when a new selection is made using the dropdown
+ function selectThis( event ){
+ var $this = $( this ),
+ $select = $this.parents( '.dropdown-select' ),
+ newSelection = $this.text();
+ $select.find( '.dropdown-select-selected' ).text( newSelection );
+ $select.trigger( 'change.dropdown-select', newSelection );
+ }
+
+ $select.find( 'a' ).click( selectThis );
+ return $select;
+ }
+
+ //==============================================================================
+ /**
+ * Creates a three part bootstrap button group (key, op, value) meant to
+ * allow the user control of filters (e.g. { key: 'name', op: 'contains', value: 'my_history' })
+ *
+ * Each field uses a dropDownSelect (from ui.js) to allow selection
+ * (with the 'value' field appearing as an input when set to do so).
+ *
+ * Any change or update in any of the fields will trigger a 'change.filter-control'
+ * event which will be passed an object containing those fields (as the example above).
+ *
+ * Pass in an array of possible filter objects to control what the user can select.
+ * Each filter object should have:
+ * key : generally the attribute name on which to filter something
+ * ops : an array of 1 or more filter operations (e.g. [ 'is', '<', 'contains', '!=' ])
+ * values (optional) : an array of possible values for the filter (e.g. [ 'true', 'false' ])
+ * @example:
+ * $( '.my-div' ).filterControl({
+ * filters : [
+ * { key: 'name', ops: [ 'is exactly', 'contains' ] }
+ * { key: 'deleted', ops: [ 'is' ], values: [ 'true', 'false' ] }
+ * ]
+ * });
+ * // after initialization, you can prog. get the current value using:
+ * $( '.my-div' ).filterControl( 'val' )
+ *
+ */
+ function FilterControl( element, options ){
+ return this.init( element, options );
+ }
+ /** the data key that this object will be stored under in the DOM element */
+ FilterControl.prototype.DATA_KEY = 'filter-control';
+
+ /** parses options, sets up instance vars, and does initial render */
+ FilterControl.prototype.init = function _init( element, options ){
+ options = options || { filters: [] };
+ this.$element = $( element ).addClass( 'filter-control btn-group' );
+ this.options = jQuery.extend( true, {}, this.defaults, options );
+
+ this.currFilter = this.options.filters[0];
+ return this.render();
+ };
+
+ /** render (or re-render) the controls on the element */
+ FilterControl.prototype.render = function _render(){
+ this.$element.empty()
+ .append([ this._renderKeySelect(), this._renderOpSelect(), this._renderValueInput() ]);
+ return this;
+ };
+
+ /** render the key dropDownSelect, bind a change event to it, and return it */
+ FilterControl.prototype._renderKeySelect = function __renderKeySelect(){
+ var filterControl = this;
+ var keys = this.options.filters.map( function( filter ){
+ return filter.key;
+ });
+ this.$keySelect = dropDownSelect( keys, this.currFilter.key )
+ .addClass( 'filter-control-key' )
+ .on( 'change.dropdown-select', function( event, selection ){
+ filterControl.currFilter = _.findWhere( filterControl.options.filters, { key: selection });
+ // when the filter/key changes, re-render the control entirely
+ filterControl.render()._triggerChange();
+ });
+ return this.$keySelect;
+ };
+
+ /** render the op dropDownSelect, bind a change event to it, and return it */
+ FilterControl.prototype._renderOpSelect = function __renderOpSelect(){
+ var filterControl = this,
+ ops = this.currFilter.ops;
+ //TODO: search for currOp in avail. ops: use that for selected if there; otherwise: first op
+ this.$opSelect = dropDownSelect( ops, ops[0] )
+ .addClass( 'filter-control-op' )
+ .on( 'change.dropdown-select', function( event, selection ){
+ filterControl._triggerChange();
+ });
+ return this.$opSelect;
+ };
+
+ /** render the value control, bind a change event to it, and return it */
+ FilterControl.prototype._renderValueInput = function __renderValueInput(){
+ var filterControl = this;
+ // if a values attribute is prov. on the filter - make this a dropdown; otherwise, use an input
+ if( this.currFilter.values ){
+ this.$valueSelect = dropDownSelect( this.currFilter.values, this.currFilter.values[0] )
+ .on( 'change.dropdown-select', function( event, selection ){
+ filterControl._triggerChange();
+ });
+ } else {
+ //TODO: allow setting a value type (mainly for which html5 input to use: range, number, etc.)
+ this.$valueSelect = $( '<input/>' ).addClass( 'form-control' )
+ .on( 'change', function( event, value ){
+ filterControl._triggerChange();
+ });
+ }
+ this.$valueSelect.addClass( 'filter-control-value' );
+ return this.$valueSelect;
+ };
+
+ /** return the current state/setting for the filter as a three key object: key, op, value */
+ FilterControl.prototype.val = function _val(){
+ var key = this.$element.find( '.filter-control-key .dropdown-select-selected' ).text(),
+ op = this.$element.find( '.filter-control-op .dropdown-select-selected' ).text(),
+ // handle either a dropdown or plain input
+ $value = this.$element.find( '.filter-control-value' ),
+ value = ( $value.hasClass( 'dropdown-select' ) )?( $value.find( '.dropdown-select-selected' ).text() )
+ :( $value.val() );
+ return { key: key, op: op, value: value };
+ };
+
+ // single point of change for change event
+ FilterControl.prototype._triggerChange = function __triggerChange(){
+ this.$element.trigger( 'change.filter-control', this.val() );
+ };
+
+ // as jq plugin
+ jQuery.fn.extend({
+ filterControl : function $filterControl( options ){
+ var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
+ return this.map( function(){
+ var $this = $( this ),
+ data = $this.data( FilterControl.prototype.DATA_KEY );
+
+ if( jQuery.type( options ) === 'object' ){
+ data = new FilterControl( $this, options );
+ $this.data( FilterControl.prototype.DATA_KEY, data );
+ }
+ if( data && jQuery.type( options ) === 'string' ){
+ var fn = data[ options ];
+ if( jQuery.type( fn ) === 'function' ){
+ return fn.apply( data, nonOptionsArgs );
+ }
+ }
+ return this;
+ });
+ }
+ });
+}));
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/mode-button.js
--- /dev/null
+++ b/client/galaxy/scripts/jq-plugins/ui/mode-button.js
@@ -0,0 +1,191 @@
+// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js
+// Uses AMD or browser globals to create a jQuery plugin.
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ //TODO: So...this turns out to be an all or nothing thing. If I load jQuery in the define below, it will
+ // (of course) wipe the old jquery *and all the plugins loaded into it*. So the define below *is still
+ // relying on jquery being loaded globally* in order to preserve plugins.
+ define([], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+
+}(function () {
+
+ /** Multi 'mode' button (or any element really) that changes the html
+ * contents of itself when clicked. Pass in an ordered list of
+ * objects with 'html' and (optional) onclick functions.
+ *
+ * When clicked in a particular node, the onclick function will
+ * be called (with the element as this) and the element will
+ * switch to the next mode, replacing its html content with
+ * that mode's html.
+ *
+ * If there is no next mode, the element will switch back to
+ * the first mode.
+ * @example:
+ * $( '.myElement' ).modeButton({
+ * modes : [
+ * {
+ * mode: 'bler',
+ * html: '<h5>Bler</h5>',
+ * onclick : function(){
+ * $( 'body' ).css( 'background-color', 'red' );
+ * }
+ * },
+ * {
+ * mode: 'bloo',
+ * html: '<h4>Bloo</h4>',
+ * onclick : function(){
+ * $( 'body' ).css( 'background-color', 'blue' );
+ * }
+ * },
+ * {
+ * mode: 'blah',
+ * html: '<h3>Blah</h3>',
+ * onclick : function(){
+ * $( 'body' ).css( 'background-color', 'grey' );
+ * }
+ * },
+ * ]
+ * });
+ * $( '.myElement' ).modeButton( 'callModeFn', 'bler' );
+ */
+ /** constructor */
+ function ModeButton( element, options ){
+ this.currModeIndex = 0;
+ return this._init( element, options );
+ }
+
+ /** html5 data key to store this object inside an element */
+ ModeButton.prototype.DATA_KEY = 'mode-button';
+ /** default options */
+ ModeButton.prototype.defaults = {
+ switchModesOnClick : true
+ };
+
+ // ---- private interface
+ /** set up options, intial mode, and the click handler */
+ ModeButton.prototype._init = function _init( element, options ){
+ //console.debug( 'ModeButton._init:', element, options );
+ options = options || {};
+ this.$element = $( element );
+ this.options = $.extend( true, {}, this.defaults, options );
+ if( !options.modes ){
+ throw new Error( 'ModeButton requires a "modes" array' );
+ }
+
+ var modeButton = this;
+ this.$element.click( function _ModeButtonClick( event ){
+ // call the curr mode fn
+ modeButton.callModeFn();
+ // inc the curr mode index
+ if( modeButton.options.switchModesOnClick ){ modeButton._incModeIndex(); }
+ // set the element html
+ $( this ).html( modeButton.options.modes[ modeButton.currModeIndex ].html );
+ });
+ return this.reset();
+ };
+ /** increment the mode index to the next in the array, looping back to zero if at the last */
+ ModeButton.prototype._incModeIndex = function _incModeIndex(){
+ this.currModeIndex += 1;
+ if( this.currModeIndex >= this.options.modes.length ){
+ this.currModeIndex = 0;
+ }
+ return this;
+ };
+ /** get the mode index in the modes array for the given key (mode name) */
+ ModeButton.prototype._getModeIndex = function _getModeIndex( modeKey ){
+ for( var i=0; i<this.options.modes.length; i+=1 ){
+ if( this.options.modes[ i ].mode === modeKey ){ return i; }
+ }
+ throw new Error( 'mode not found: ' + modeKey );
+ };
+ /** set the current mode to the one with the given index and set button html */
+ ModeButton.prototype._setModeByIndex = function _setModeByIndex( index ){
+ var newMode = this.options.modes[ index ];
+ if( !newMode ){
+ throw new Error( 'mode index not found: ' + index );
+ }
+ this.currModeIndex = index;
+ if( newMode.html ){
+ this.$element.html( newMode.html );
+ }
+ return this;
+ };
+
+ // ---- public interface
+ /** get the current mode object (not just the mode name) */
+ ModeButton.prototype.currentMode = function currentMode(){
+ return this.options.modes[ this.currModeIndex ];
+ };
+ /** return the mode key of the current mode */
+ ModeButton.prototype.current = function current(){
+ // sugar for returning mode name
+ return this.currentMode().mode;
+ };
+ /** get the mode with the given modeKey or the current mode if modeKey is undefined */
+ ModeButton.prototype.getMode = function getMode( modeKey ){
+ if( !modeKey ){ return this.currentMode(); }
+ return this.options.modes[( this._getModeIndex( modeKey ) )];
+ };
+ /** T/F if the button has the given mode */
+ ModeButton.prototype.hasMode = function hasMode( modeKey ){
+ try {
+ return !!this.getMode( modeKey );
+ } catch( err ){}
+ return false;
+ };
+ /** set the current mode to the mode with the given name */
+ ModeButton.prototype.setMode = function setMode( modeKey ){
+ return this._setModeByIndex( this._getModeIndex( modeKey ) );
+ };
+ /** reset to the initial mode */
+ ModeButton.prototype.reset = function reset(){
+ this.currModeIndex = 0;
+ if( this.options.initialMode ){
+ this.currModeIndex = this._getModeIndex( this.options.initialMode );
+ }
+ return this._setModeByIndex( this.currModeIndex );
+ };
+ /** manually call the click handler of the given mode */
+ ModeButton.prototype.callModeFn = function callModeFn( modeKey ){
+ var modeFn = this.getMode( modeKey ).onclick;
+ if( modeFn && $.type( modeFn === 'function' ) ){
+ // call with the element as context (std jquery pattern)
+ return modeFn.call( this.$element.get(0) );
+ }
+ return undefined;
+ };
+
+ // as jq plugin
+ $.fn.modeButton = function $modeButton( options ){
+ if( !this.size() ){ return this; }
+
+ //TODO: does map still work with jq multi selection (i.e. $( '.class-for-many-btns' ).modeButton)?
+ if( $.type( options ) === 'object' ){
+ return this.map( function(){
+ var $this = $( this );
+ $this.data( 'mode-button', new ModeButton( $this, options ) );
+ return this;
+ });
+ }
+
+ var $first = $( this[0] ),
+ button = $first.data( 'mode-button' );
+
+ if( !button ){
+ throw new Error( 'modeButton needs an options object or string name of a function' );
+ }
+
+ if( button && $.type( options ) === 'string' ){
+ var fnName = options;
+ if( button && $.type( button[ fnName ] ) === 'function' ){
+ return button[ fnName ].apply( button, $.makeArray( arguments ).slice( 1 ) );
+ }
+ }
+ return button;
+ };
+
+}));
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/pagination.js
--- /dev/null
+++ b/client/galaxy/scripts/jq-plugins/ui/pagination.js
@@ -0,0 +1,226 @@
+// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js
+// Uses AMD or browser globals to create a jQuery plugin.
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+
+}(function ($) {
+
+ /** Builds (twitter bootstrap styled) pagination controls.
+ * If the totalDataSize is not null, a horizontal list of page buttons is displayed.
+ * If totalDataSize is null, two links ('Prev' and 'Next) are displayed.
+ * When pages are changed, a 'pagination.page-change' event is fired
+ * sending the event and the (0-based) page requested.
+ */
+ function Pagination( element, options ){
+ /** the total number of pages */
+ this.numPages = null;
+ /** the current, active page */
+ this.currPage = 0;
+ return this.init( element, options );
+ }
+
+ /** data key under which this object will be stored in the element */
+ Pagination.prototype.DATA_KEY = 'pagination';
+ /** default options */
+ Pagination.prototype.defaults = {
+ /** which page to begin at */
+ startingPage : 0,
+ /** number of data per page */
+ perPage : 20,
+ /** the total number of data (null == unknown) */
+ totalDataSize : null,
+ /** size of current data on current page */
+ currDataSize : null
+ };
+
+ /** init the control, calc numPages if possible, and render
+ * @param {jQuery} the element that will contain the pagination control
+ * @param {Object} options a map containing overrides to the pagination default options
+ */
+ Pagination.prototype.init = function _init( $element, options ){
+ options = options || {};
+ this.$element = $element;
+ this.options = jQuery.extend( true, {}, this.defaults, options );
+
+ this.currPage = this.options.startingPage;
+ if( this.options.totalDataSize !== null ){
+ this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage );
+ // limit currPage by numPages
+ if( this.currPage >= this.numPages ){
+ this.currPage = this.numPages - 1;
+ }
+ }
+ //console.debug( 'Pagination.prototype.init:', this.$element, this.currPage );
+ //console.debug( JSON.stringify( this.options ) );
+
+ // bind to data of element
+ this.$element.data( Pagination.prototype.DATA_KEY, this );
+
+ this._render();
+ return this;
+ };
+
+ /** helper to create a simple li + a combo */
+ function _make$Li( contents ){
+ return $([
+ '<li><a href="javascript:void(0);">', contents, '</a></li>'
+ ].join( '' ));
+ }
+
+ /** render previous and next pagination buttons */
+ Pagination.prototype._render = function __render(){
+ // no data - no pagination
+ if( this.options.totalDataSize === 0 ){ return this; }
+ // only one page
+ if( this.numPages === 1 ){ return this; }
+
+ // when the number of pages are known, render each page as a link
+ if( this.numPages > 0 ){
+ this._renderPages();
+ this._scrollToActivePage();
+
+ // when the number of pages is not known, render previous or next
+ } else {
+ this._renderPrevNext();
+ }
+ return this;
+ };
+
+ /** render previous and next pagination buttons */
+ Pagination.prototype._renderPrevNext = function __renderPrevNext(){
+ var pagination = this,
+ $prev = _make$Li( 'Prev' ),
+ $next = _make$Li( 'Next' ),
+ $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' );
+
+ // disable if it either end
+ if( this.currPage === 0 ){
+ $prev.addClass( 'disabled' );
+ } else {
+ $prev.click( function(){ pagination.prevPage(); });
+ }
+ if( ( this.numPages && this.currPage === ( this.numPages - 1 ) )
+ || ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){
+ $next.addClass( 'disabled' );
+ } else {
+ $next.click( function(){ pagination.nextPage(); });
+ }
+
+ this.$element.html( $paginationContainer.append([ $prev, $next ]) );
+ //console.debug( this.$element, this.$element.html() );
+ return this.$element;
+ };
+
+ /** render page links for each possible page (if we can) */
+ Pagination.prototype._renderPages = function __renderPages(){
+ // it's better to scroll the control and let the user see all pages
+ // than to force her/him to change pages in order to find the one they want (as traditional << >> does)
+ var pagination = this,
+ $scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ),
+ $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ),
+ page$LiClick = function( ev ){
+ pagination.goToPage( $( this ).data( 'page' ) );
+ };
+
+ for( var i=0; i<this.numPages; i+=1 ){
+ // add html5 data tag 'page' for later click event handler use
+ var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick );
+ // highlight the current page
+ if( i === this.currPage ){
+ $pageLi.addClass( 'active' );
+ }
+ //console.debug( '\t', $pageLi );
+ $paginationContainer.append( $pageLi );
+ }
+ return this.$element.html( $scrollingContainer.html( $paginationContainer ) );
+ };
+
+ /** scroll scroll-container (if any) to show the active page */
+ Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){
+ // scroll to show active page in center of scrollable area
+ var $container = this.$element.find( '.pagination-scroll-container' );
+ // no scroll container : don't scroll
+ if( !$container.size() ){ return this; }
+
+ var $activePage = this.$element.find( 'li.active' ),
+ midpoint = $container.width() / 2;
+ //console.debug( $container, $activePage, midpoint );
+ $container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint );
+ return this;
+ };
+
+ /** go to a certain page */
+ Pagination.prototype.goToPage = function goToPage( page ){
+ if( page <= 0 ){ page = 0; }
+ if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; }
+ if( page === this.currPage ){ return this; }
+
+ //console.debug( '\t going to page ' + page )
+ this.currPage = page;
+ this.$element.trigger( 'pagination.page-change', this.currPage );
+ //console.info( 'pagination:page-change', this.currPage );
+ this._render();
+ return this;
+ };
+
+ /** go to the previous page */
+ Pagination.prototype.prevPage = function prevPage(){
+ return this.goToPage( this.currPage - 1 );
+ };
+
+ /** go to the next page */
+ Pagination.prototype.nextPage = function nextPage(){
+ return this.goToPage( this.currPage + 1 );
+ };
+
+ /** return the current page */
+ Pagination.prototype.page = function page(){
+ return this.currPage;
+ };
+
+ // alternate constructor invocation
+ Pagination.create = function _create( $element, options ){
+ return new Pagination( $element, options );
+ };
+
+ // as jq plugin
+ jQuery.fn.extend({
+ pagination : function $pagination( options ){
+ var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 );
+
+ // if passed an object - use that as an options map to create pagination for each selected
+ if( jQuery.type( options ) === 'object' ){
+ return this.map( function(){
+ Pagination.create( $( this ), options );
+ return this;
+ });
+ }
+
+ // (other invocations only work on the first element in selected)
+ var $firstElement = $( this[0] ),
+ previousControl = $firstElement.data( Pagination.prototype.DATA_KEY );
+ // if a pagination control was found for this element, either...
+ if( previousControl ){
+ // invoke a function on the pagination object if passed a string (the function name)
+ if( jQuery.type( options ) === 'string' ){
+ var fn = previousControl[ options ];
+ if( jQuery.type( fn ) === 'function' ){
+ return fn.apply( previousControl, nonOptionsArgs );
+ }
+
+ // if passed nothing, return the previously set control
+ } else {
+ return previousControl;
+ }
+ }
+ // if there is no control already set, return undefined
+ return undefined;
+ }
+ });
+}));
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js
--- /dev/null
+++ b/client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js
@@ -0,0 +1,317 @@
+// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js
+// Uses AMD or browser globals to create a jQuery plugin.
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+
+}(function ($) {
+ //==============================================================================
+ /** Column selection using the peek display as the control.
+ * Adds rows to the bottom of the peek with clickable areas in each cell
+ * to allow the user to select columns.
+ * Column selection can be limited to a single column or multiple.
+ * (Optionally) adds a left hand column of column selection prompts.
+ * (Optionally) allows the column headers to be clicked/renamed
+ * and set to some initial value.
+ * (Optionally) hides comment rows.
+ * (Optionally) allows pre-selecting and disabling certain columns for
+ * each row control.
+ *
+ * Construct by selecting a peek table to be used with jQuery and
+ * calling 'peekColumnSelector' with options.
+ * Options must include a 'controls' array and can include other options
+ * listed below.
+ * @example:
+ * $( 'pre.peek' ).peekColumnSelector({
+ * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ],
+ * controls : [
+ * { label: 'X Column', id: 'xColumn' },
+ * { label: 'Y Column', id: 'yColumn', selected: 2 },
+ * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] },
+ * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true,
+ * selectedText: 'Included', unselectedText: 'Excluded' }
+ * ],
+ * renameColumns : true,
+ * hideCommentRows : true,
+ * includePrompts : true,
+ * topLeftContent : 'Data sample:'
+ * }).on( 'peek-column-selector.change', function( ev, selection ){
+ * console.info( 'new selection:', selection );
+ * //{ yColumn: 2 }
+ * }).on( 'peek-column-selector.rename', function( ev, names ){
+ * console.info( 'column names', names );
+ * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ]
+ * });
+ *
+ * An event is fired when column selection is changed and the event
+ * is passed an object in the form: { the row id : the new selection value }.
+ * An event is also fired when the table headers are re-named and
+ * is passed the new array of column names.
+ */
+
+ /** option defaults */
+ var defaults = {
+ /** does this control allow renaming headers? */
+ renameColumns : false,
+ /** does this control allow renaming headers? */
+ columnNames : [],
+ /** the comment character used by the peek's datatype */
+ commentChar : '#',
+ /** should comment rows be shown or hidden in the peek */
+ hideCommentRows : false,
+ /** should a column of row control prompts be used */
+ includePrompts : true,
+ /** what is the content of the top left cell (often a title) */
+ topLeftContent : 'Columns:'
+ },
+ /** class added to the pre.peek element (to allow css on just the control) */
+ PEEKCONTROL_CLASS = 'peek-column-selector',
+ /** the string of the event fired when a control row changes */
+ CHANGE_EVENT = 'peek-column-selector.change',
+ /** the string of the event fired when a column is renamed */
+ RENAME_EVENT = 'peek-column-selector.rename',
+ /** class added to the control rows */
+ ROW_CLASS = 'control',
+ /** class added to the left-hand cells that serve as row prompts */
+ PROMPT_CLASS = 'control-prompt',
+ /** class added to selected _cells_/tds */
+ SELECTED_CLASS = 'selected',
+ /** class added to disabled/un-clickable cells/tds */
+ DISABLED_CLASS = 'disabled',
+ /** class added to the clickable surface within a cell to select it */
+ BUTTON_CLASS = 'button',
+ /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */
+ RENAMABLE_HEADER_CLASS = 'renamable-header',
+ /** the data key used for each cell to store the column index ('data-...') */
+ COLUMN_INDEX_DATA_KEY = 'column-index',
+ /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */
+ COLUMN_NAME_DATA_KEY = 'column-name';
+
+ //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.)
+ //TODO: needs clean up, move handlers to outer scope
+
+ // ........................................................................
+ /** validate the control data sent in for each row */
+ function validateControl( control ){
+ if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){
+ throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) );
+ }
+ if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){
+ throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) );
+ }
+ if( !control.label || !control.id ){
+ throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) );
+ }
+ if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){
+ throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) );
+ }
+ return control;
+ }
+
+ /** build the inner control surface (i.e. button-like) */
+ function buildButton( control, columnIndex ){
+ return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label );
+ }
+
+ /** build the basic (shared) cell structure */
+ function buildControlCell( control, columnIndex ){
+ var $td = $( '<td/>' )
+ .html( buildButton( control, columnIndex ) )
+ .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex );
+
+ // disable if index in disabled array
+ if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){
+ $td.addClass( DISABLED_CLASS );
+ }
+ return $td;
+ }
+
+ /** set the text of the control based on selected/un */
+ function setSelectedText( $cell, control, columnIndex ){
+ var $button = $cell.children( '.' + BUTTON_CLASS );
+ if( $cell.hasClass( SELECTED_CLASS ) ){
+ $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) );
+ } else {
+ $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) );
+ }
+ }
+
+ /** build a cell for a row that only allows one selection */
+ function buildSingleSelectCell( control, columnIndex ){
+ // only one selection - selected is single index
+ var $cell = buildControlCell( control, columnIndex );
+ if( control.selected === columnIndex ){
+ $cell.addClass( SELECTED_CLASS );
+ }
+ setSelectedText( $cell, control, columnIndex );
+
+ // only add the handler to non-disabled controls
+ if( !$cell.hasClass( DISABLED_CLASS ) ){
+ $cell.click( function selectClick( ev ){
+ var $cell = $( this );
+ // don't re-select or fire event if already selected
+ if( !$cell.hasClass( SELECTED_CLASS ) ){
+ // only one can be selected - remove selected on all others, add it here
+ var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS );
+ $otherSelected.each( function(){
+ setSelectedText( $( this ), control, columnIndex );
+ });
+
+ $cell.addClass( SELECTED_CLASS );
+ setSelectedText( $cell, control, columnIndex );
+
+ // fire the event from the table itself, passing the id and index of selected
+ var eventData = {},
+ key = $cell.parent().attr( 'id' ),
+ val = $cell.data( COLUMN_INDEX_DATA_KEY );
+ eventData[ key ] = val;
+ $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData );
+ }
+ });
+ }
+ return $cell;
+ }
+
+ /** build a cell for a row that allows multiple selections */
+ function buildMultiSelectCell( control, columnIndex ){
+ var $cell = buildControlCell( control, columnIndex );
+ // multiple selection - selected is an array
+ if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){
+ $cell.addClass( SELECTED_CLASS );
+ }
+ setSelectedText( $cell, control, columnIndex );
+
+ // only add the handler to non-disabled controls
+ if( !$cell.hasClass( DISABLED_CLASS ) ){
+ $cell.click( function multiselectClick( ev ){
+ var $cell = $( this );
+ // can be more than one selected - toggle selected on this cell
+ $cell.toggleClass( SELECTED_CLASS );
+ setSelectedText( $cell, control, columnIndex );
+ var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){
+ return $( e ).data( COLUMN_INDEX_DATA_KEY );
+ });
+ // fire the event from the table itself, passing the id and index of selected
+ var eventData = {},
+ key = $cell.parent().attr( 'id' ),
+ val = jQuery.makeArray( selectedColumnIndeces );
+ eventData[ key ] = val;
+ $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData );
+ });
+ }
+ return $cell;
+ }
+
+ /** iterate over columns in peek and create a control for each */
+ function buildControlCells( count, control ){
+ var $cells = [];
+ // build a control for each column - using a build fn based on control
+ for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){
+ $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex )
+ : buildSingleSelectCell( control, columnIndex ) );
+ }
+ return $cells;
+ }
+
+ /** build a row of controls for the peek */
+ function buildControlRow( cellCount, control, includePrompts ){
+ var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS );
+ if( includePrompts ){
+ var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' );
+ $controlRow.append( $promptCell );
+ }
+ $controlRow.append( buildControlCells( cellCount, control ) );
+ return $controlRow;
+ }
+
+ // ........................................................................
+ /** add to the peek, using options for configuration, return the peek */
+ function peekColumnSelector( options ){
+ options = jQuery.extend( true, {}, defaults, options );
+
+ var $peek = $( this ).addClass( PEEKCONTROL_CLASS ),
+ $peektable = $peek.find( 'table' ),
+ // get the size of the tables - width and height, number of comment rows
+ columnCount = $peektable.find( 'th' ).size(),
+ rowCount = $peektable.find( 'tr' ).size(),
+ // get the rows containing text starting with the comment char (also make them grey)
+ $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){
+ var $this = $( this );
+ if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){
+ return $( this ).css( 'color', 'grey' ).parent().get(0);
+ }
+ return null;
+ });
+
+ // should comment rows in the peek be hidden?
+ if( options.hideCommentRows ){
+ $commentRows.hide();
+ rowCount -= $commentRows.size();
+ }
+ //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows );
+
+ // should a first column of control prompts be added?
+ if( options.includePrompts ){
+ var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent )
+ .attr( 'rowspan', rowCount );
+ $peektable.find( 'tr' ).first().prepend( $topLeft );
+ }
+
+ // save either the options column name or the parsed text of each column header in html5 data attr and text
+ var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){
+ var $this = $( this ),
+ // can be '1.name' or '1'
+ text = $this.text().replace( /^\d+\.*/, '' ),
+ name = options.columnNames[ i ] || text;
+ $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name )
+ .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) );
+ });
+
+ // allow renaming of columns when the header is clicked
+ if( options.renameColumns ){
+ $headers.addClass( RENAMABLE_HEADER_CLASS )
+ .click( function renameColumn(){
+ // prompt for new name
+ var $this = $( this ),
+ index = $this.index() + ( options.includePrompts? 0: 1 ),
+ prevName = $this.data( COLUMN_NAME_DATA_KEY ),
+ newColumnName = prompt( 'New column name:', prevName );
+ if( newColumnName !== null && newColumnName !== prevName ){
+ // set the new text and data
+ $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) )
+ .data( COLUMN_NAME_DATA_KEY, newColumnName )
+ .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName );
+ // fire event for new column names
+ var columnNames = jQuery.makeArray(
+ $this.parent().children( 'th:not(.top-left)' ).map( function(){
+ return $( this ).data( COLUMN_NAME_DATA_KEY );
+ }));
+ $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames );
+ }
+ });
+ }
+
+ // build a row for each control
+ options.controls.forEach( function( control, i ){
+ validateControl( control );
+ var $controlRow = buildControlRow( columnCount, control, options.includePrompts );
+ $peektable.find( 'tbody' ).append( $controlRow );
+ });
+ return this;
+ }
+
+ // ........................................................................
+ // as jq plugin
+ jQuery.fn.extend({
+ peekColumnSelector : function $peekColumnSelector( options ){
+ return this.map( function(){
+ return peekColumnSelector.call( this, options );
+ });
+ }
+ });
+}));
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/history/multi-panel.js
--- a/client/galaxy/scripts/mvc/history/multi-panel.js
+++ b/client/galaxy/scripts/mvc/history/multi-panel.js
@@ -2,7 +2,8 @@
"mvc/history/history-model",
"mvc/history/history-panel-edit",
"mvc/base-mvc",
- "utils/ajax-queue"
+ "utils/ajax-queue",
+ "jq-plugins/ui/mode-button"
], function( HISTORY_MODEL, HPANEL_EDIT, baseMVC, ajaxQueue ){
window.HISTORY_MODEL = HISTORY_MODEL;
//==============================================================================
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/library/library-folderlist-view.js
--- a/client/galaxy/scripts/mvc/library/library-folderlist-view.js
+++ b/client/galaxy/scripts/mvc/library/library-folderlist-view.js
@@ -15,322 +15,359 @@
) {
var FolderListView = Backbone.View.extend({
- el : '#folder_items_element',
- defaults: {
- 'include_deleted' : false
- },
- // progress percentage
- progress: 0,
- // progress rate per one item
- progressStep: 1,
- // self modal
- modal : null,
+ el : '#folder_items_element',
+ // progress percentage
+ progress: 0,
+ // progress rate per one item
+ progressStep: 1,
- folderContainer: null,
+ folderContainer: null,
- sort: 'asc',
+ sort: 'asc',
- events: {
- 'click #select-all-checkboxes' : 'selectAll',
- 'click .dataset_row' : 'selectClickedRow',
- 'click .sort-folder-link' : 'sortColumnClicked'
- },
+ events: {
+ 'click #select-all-checkboxes' : 'selectAll',
+ 'click .dataset_row' : 'selectClickedRow',
+ 'click .sort-folder-link' : 'sortColumnClicked'
+ },
- // cache of rendered views
- rowViews: {},
-
- initialize : function(options){
- this.options = _.defaults(this.options || {}, options);
- this.fetchFolder();
- },
+ collection: null,
- fetchFolder: function(options){
- var options = options || {};
- this.options.include_deleted = options.include_deleted;
- var that = this;
+ defaults: {
+ include_deleted: false,
+ page_count: null,
+ show_page: null
+ },
- this.collection = new mod_library_model.Folder();
+ /**
+ * Initialize and fetch the folder from the server.
+ * @param {object} options an object with options
+ */
+ initialize : function( options ){
+ this.options = _.defaults( this.options || {}, this.defaults, options );
+ this.modal = null;
+ // map of folder item ids to item views = cache
+ this.rowViews = {};
- // start to listen if someone modifies collection
- this.listenTo(this.collection, 'add', this.renderOne);
- this.listenTo(this.collection, 'remove', this.removeOne);
+ // create a collection of folder items for this view
+ this.collection = new mod_library_model.Folder();
- this.folderContainer = new mod_library_model.FolderContainer({id: this.options.id});
- this.folderContainer.url = this.folderContainer.attributes.urlRoot + this.options.id + '/contents';
- if (this.options.include_deleted){
- this.folderContainer.url = this.folderContainer.url + '?include_deleted=true';
- }
- this.folderContainer.fetch({
- success: function(folder_container) {
- that.folder_container = folder_container;
- that.render();
- that.addAll(folder_container.get('folder').models);
- if (that.options.dataset_id){
- row = _.findWhere(that.rowViews, {id: that.options.dataset_id});
- if (row) {
+ // start to listen if someone modifies the collection
+ this.listenTo( this.collection, 'add', this.renderOne );
+ this.listenTo( this.collection, 'remove', this.removeOne );
+
+ this.fetchFolder();
+ },
+
+ fetchFolder: function( options ){
+ var options = options || {};
+ this.options.include_deleted = options.include_deleted;
+ var that = this;
+
+ this.folderContainer = new mod_library_model.FolderContainer( { id: this.options.id } );
+ this.folderContainer.url = this.folderContainer.attributes.urlRoot + this.options.id + '/contents';
+
+ if ( this.options.include_deleted ){
+ this.folderContainer.url = this.folderContainer.url + '?include_deleted=true';
+ }
+ this.folderContainer.fetch({
+ success: function( folder_container ) {
+ that.folder_container = folder_container;
+ that.render();
+ },
+ error: function( model, response ){
+ if ( typeof response.responseJSON !== "undefined" ){
+ mod_toastr.error( response.responseJSON.err_msg + ' Click this to go back.', '', { onclick: function() { Galaxy.libraries.library_router.back(); } } );
+ } else {
+ mod_toastr.error( 'An error ocurred. Click this to go back.', '', { onclick: function() { Galaxy.libraries.library_router.back(); } } );
+ }
+ }
+ });
+ },
+
+ render: function ( options ){
+ this.options = _.extend( this.options, options );
+ var template = this.templateFolder();
+ $(".tooltip").hide();
+
+ // find the upper id in the full path
+ var path = this.folderContainer.attributes.metadata.full_path;
+ var upper_folder_id;
+ if ( path.length === 1 ){ // the library is above us
+ upper_folder_id = 0;
+ } else {
+ upper_folder_id = path[ path.length-2 ][ 0 ];
+ }
+
+ this.$el.html( template( {
+ path: this.folderContainer.attributes.metadata.full_path,
+ parent_library_id: this.folderContainer.attributes.metadata.parent_library_id,
+ id: this.options.id,
+ upper_folder_id: upper_folder_id,
+ order: this.sort
+ } ) );
+
+ // when dataset_id is present render its details too
+ if ( this.options.dataset_id ){
+ row = _.findWhere( that.rowViews, { id: this.options.dataset_id } );
+ if ( row ) {
row.showDatasetDetails();
} else {
- mod_toastr.error('Dataset not found. Showing folder instead.');
+ mod_toastr.error( 'Requested dataset not found. Showing folder instead.' );
}
+ } else {
+ if ( this.options.show_page === null || this.options.show_page < 1 ){
+ this.options.show_page = 1;
+ }
+ this.paginate();
+ }
+ $("#center [data-toggle]").tooltip();
+ $("#center").css('overflow','auto');
+ },
+
+ paginate: function( options ){
+ this.options = _.extend( this.options, options );
+
+ if ( this.options.show_page === null || this.options.show_page < 1 ){
+ this.options.show_page = 1;
+ }
+ this.options.total_items_count = this.folder_container.get( 'folder' ).models.length;
+ this.options.page_count = Math.ceil( this.options.total_items_count / Galaxy.libraries.preferences.get( 'folder_page_size' ) );
+ var page_start = ( Galaxy.libraries.preferences.get( 'folder_page_size' ) * ( this.options.show_page - 1 ) );
+ var items_to_render = null;
+ items_to_render = this.folder_container.get( 'folder' ).models.slice( page_start, page_start + Galaxy.libraries.preferences.get( 'folder_page_size' ) );
+ this.options.items_shown = items_to_render.length;
+ // User requests page with no items
+ if ( Galaxy.libraries.preferences.get( 'folder_page_size' ) * this.options.show_page > ( this.options.total_items_count + Galaxy.libraries.preferences.get( 'folder_page_size' ) ) ){
+ items_to_render = [];
+ }
+ Galaxy.libraries.folderToolbarView.renderPaginator( this.options );
+ this.collection.reset();
+ this.addAll( items_to_render )
+ },
+
+ /**
+ * Adds all given models to the collection.
+ * @param {array of Item or FolderAsModel} array of models that should
+ * be added to the view's collection.
+ */
+ addAll: function( models ){
+ _.each(models, function( model ) {
+ Galaxy.libraries.folderListView.collection.add( model );
+ });
+ $( "#center [data-toggle]" ).tooltip();
+ this.checkEmptiness();
+ this.postRender();
+ },
+
+ /**
+ * Call this after all models are added to the collection
+ * to ensure that the folder toolbar will show proper options
+ * and that event will be bound on all subviews.
+ */
+ postRender: function(){
+ var fetched_metadata = this.folderContainer.attributes.metadata;
+ fetched_metadata.contains_file = typeof this.collection.findWhere({type: 'file'}) !== 'undefined';
+ Galaxy.libraries.folderToolbarView.configureElements(fetched_metadata);
+ $('.library-row').hover(function() {
+ $(this).find('.show_on_hover').show();
+ }, function () {
+ $(this).find('.show_on_hover').hide();
+ });
+ },
+
+ /**
+ * Iterates this view's collection and calls the render
+ * function for each. Also binds the hover behavior.
+ */
+ renderAll: function(){
+ var that = this;
+ _.each( this.collection.models.reverse(), function( model ) {
+ that.renderOne( model );
+ });
+ this.postRender();
+ },
+
+ /**
+ * Creates a view for the given model and adds it to the folder view.
+ * @param {Item or FolderAsModel} model of the view that will be rendered
+ */
+ renderOne: function(model){
+ if (model.get('type') !== 'folder'){
+ this.options.contains_file = true;
+ // model.set('readable_size', this.size_to_string(model.get('file_size')));
}
- },
- error: function(model, response){
- if (typeof response.responseJSON !== "undefined"){
- mod_toastr.error(response.responseJSON.err_msg + ' Click this to go back.', '', {onclick: function() {Galaxy.libraries.library_router.back();}});
- } else {
- mod_toastr.error('An error ocurred. Click this to go back.', '', {onclick: function() {Galaxy.libraries.library_router.back();}});
- }
+ model.set('folder_id', this.id);
+ var rowView = new mod_library_folderrow_view.FolderRowView(model);
+
+ // save new rowView to cache
+ this.rowViews[model.get('id')] = rowView;
+
+ this.$el.find('#first_folder_item').after(rowView.el);
+
+ $('.library-row').hover(function() {
+ $(this).find('.show_on_hover').show();
+ }, function () {
+ $(this).find('.show_on_hover').hide();
+ });
+ },
+
+ /**
+ * removes the view of the given model from the DOM
+ * @param {Item or FolderAsModel} model of the view that will be removed
+ */
+ removeOne: function( model ){
+ this.$el.find( '#' + model.id ).remove();
+ },
+
+ /** Checks whether the list is empty and adds/removes the message */
+ checkEmptiness : function(){
+ if ((this.$el.find('.dataset_row').length === 0) && (this.$el.find('.folder_row').length === 0)){
+ this.$el.find('.empty-folder-message').show();
+ } else {
+ this.$el.find('.empty-folder-message').hide();
}
- });
- },
+ },
- render: function (options) {
- this.options = _.defaults(this.options, options);
- var template = this.templateFolder();
- $(".tooltip").hide();
+ /** User clicked the table heading = he wants to sort stuff */
+ sortColumnClicked : function(event){
+ event.preventDefault();
+ if (this.sort === 'asc'){
+ this.sortFolder('name','desc');
+ this.sort = 'desc';
+ } else {
+ this.sortFolder('name','asc');
+ this.sort = 'asc';
+ }
+ this.render();
+ this.renderAll();
+ this.checkEmptiness();
+ },
- // TODO move to server
- // find the upper id in the full path
- var path = this.folderContainer.attributes.metadata.full_path;
- var upper_folder_id;
- if (path.length === 1){ // the library is above us
- upper_folder_id = 0;
- } else {
- upper_folder_id = path[path.length-2][0];
+ /**
+ * Sorts the underlying collection according to the parameters received.
+ * Currently supports only sorting by name.
+ */
+ sortFolder: function(sort_by, order){
+ if (sort_by === 'name'){
+ if (order === 'asc'){
+ return this.collection.sortByNameAsc();
+ } else if (order === 'desc'){
+ return this.collection.sortByNameDesc();
+ }
+ }
+ },
+
+ /**
+ * User clicked the checkbox in the table heading
+ * @param {context} event
+ */
+ selectAll : function (event) {
+ var selected = event.target.checked;
+ that = this;
+ // Iterate each checkbox
+ $(':checkbox', '#folder_list_body').each(function() {
+ this.checked = selected;
+ $row = $(this.parentElement.parentElement);
+ // Change color of selected/unselected
+ if (selected) {
+ that.makeDarkRow($row);
+ } else {
+ that.makeWhiteRow($row);
+ }
+ });
+ },
+
+ /**
+ * Check checkbox if user clicks on the whole row or
+ * on the checkbox itself
+ */
+ selectClickedRow : function (event) {
+ var checkbox = '';
+ var $row;
+ var source;
+ if (event.target.localName === 'input'){
+ checkbox = event.target;
+ $row = $(event.target.parentElement.parentElement);
+ source = 'input';
+ } else if (event.target.localName === 'td') {
+ checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0];
+ $row = $(event.target.parentElement);
+ source = 'td';
+ }
+ if (checkbox.checked){
+ if (source==='td'){
+ checkbox.checked = '';
+ this.makeWhiteRow($row);
+ } else if (source==='input') {
+ this.makeDarkRow($row);
+ }
+ } else {
+ if (source==='td'){
+ checkbox.checked = 'selected';
+ this.makeDarkRow($row);
+ } else if (source==='input') {
+ this.makeWhiteRow($row);
+ }
+ }
+ },
+
+ makeDarkRow: function($row){
+ $row.removeClass('light').addClass('dark');
+ $row.find('a').removeClass('light').addClass('dark');
+ $row.find('.fa-file-o').removeClass('fa-file-o').addClass('fa-file');
+ },
+
+ makeWhiteRow: function($row){
+ $row.removeClass('dark').addClass('light');
+ $row.find('a').removeClass('dark').addClass('light');
+ $row.find('.fa-file').removeClass('fa-file').addClass('fa-file-o');
+ },
+
+ templateFolder : function (){
+ var tmpl_array = [];
+
+ // BREADCRUMBS
+ tmpl_array.push('<ol class="breadcrumb">');
+ tmpl_array.push(' <li><a title="Return to the list of libraries" href="#">Libraries</a></li>');
+ tmpl_array.push(' <% _.each(path, function(path_item) { %>');
+ tmpl_array.push(' <% if (path_item[0] != id) { %>');
+ tmpl_array.push(' <li><a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a></li> ');
+ tmpl_array.push( '<% } else { %>');
+ tmpl_array.push(' <li class="active"><span title="You are in this folder"><%- path_item[1] %></span></li>');
+ tmpl_array.push(' <% } %>');
+ tmpl_array.push(' <% }); %>');
+ tmpl_array.push('</ol>');
+
+ // FOLDER CONTENT
+ tmpl_array.push('<table data-library-id="<%- parent_library_id %>" id="folder_table" class="grid table table-condensed">');
+ tmpl_array.push(' <thead>');
+ tmpl_array.push(' <th class="button_heading"></th>');
+ tmpl_array.push(' <th style="text-align: center; width: 20px; " title="Check to select all datasets"><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>');
+ tmpl_array.push(' <th><a class="sort-folder-link" title="Click to reverse order" href="#">name</a><span title="Sorted alphabetically" class="fa fa-sort-alpha-<%- order %>"></span></th>');
+ tmpl_array.push(' <th style="width:5%;">data type</th>');
+ tmpl_array.push(' <th style="width:10%;">size</th>');
+ tmpl_array.push(' <th style="width:160px;">time updated (UTC)</th>');
+ tmpl_array.push(' <th style="width:10%;"></th> ');
+ tmpl_array.push(' </thead>');
+ tmpl_array.push(' <tbody id="folder_list_body">');
+ tmpl_array.push(' <tr id="first_folder_item">');
+ tmpl_array.push(' <td><a href="#<% if (upper_folder_id !== 0){ print("folders/" + upper_folder_id)} %>" title="Go to parent folder" class="btn_open_folder btn btn-default btn-xs">..<a></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' <td></td>');
+ tmpl_array.push(' </tr>');
+
+ tmpl_array.push(' </tbody>');
+ tmpl_array.push('</table>');
+ tmpl_array.push('<div class="empty-folder-message" style="display:none;">This folder is either empty or you do not have proper access permissions to see the contents. If you expected something to show up please consult the <a href="https://wiki.galaxyproject.org/Admin/DataLibraries/LibrarySecurity" target="_blank">library security wikipage</a> or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.</div>');
+
+ return _.template(tmpl_array.join(''));
}
-
- this.$el.html(template({ path: this.folderContainer.attributes.metadata.full_path, parent_library_id: this.folderContainer.attributes.metadata.parent_library_id, id: this.options.id, upper_folder_id: upper_folder_id, order: this.sort}));
-
- // initialize the library tooltips
- $("#center [data-toggle]").tooltip();
- //hack to show scrollbars
- $("#center").css('overflow','auto');
- },
-
- /**
- * Call this after all models are added to the collection
- * to ensure that the folder toolbar will show proper options
- * and that event will be bound on all subviews.
- */
- postRender: function(){
- var fetched_metadata = this.folderContainer.attributes.metadata;
- fetched_metadata.contains_file = typeof this.collection.findWhere({type: 'file'}) !== 'undefined';
- Galaxy.libraries.folderToolbarView.configureElements(fetched_metadata);
- $('.library-row').hover(function() {
- $(this).find('.show_on_hover').show();
- }, function () {
- $(this).find('.show_on_hover').hide();
- });
- },
-
- /**
- * Adds all given models to the collection.
- * @param {array of Item or FolderAsModel} array of models that should
- * be added to the view's collection.
- */
- addAll: function(models){
- _.each(models.reverse(), function(model) {
- Galaxy.libraries.folderListView.collection.add(model);
- });
-
- $("#center [data-toggle]").tooltip();
- this.checkEmptiness();
-
- this.postRender();
- },
-
- /**
- * Iterates this view's collection and calls the render
- * function for each. Also binds the hover behavior.
- */
- renderAll: function(){
- var that = this;
- _.each(this.collection.models.reverse(), function(model) {
- that.renderOne(model);
- });
- this.postRender();
- },
-
- /**
- * Creates a view for the given model and adds it to the folder view.
- * @param {Item or FolderAsModel} model of the view that will be rendered
- */
- renderOne: function(model){
- if (model.get('type') !== 'folder'){
- this.options.contains_file = true;
- // model.set('readable_size', this.size_to_string(model.get('file_size')));
- }
- model.set('folder_id', this.id);
- var rowView = new mod_library_folderrow_view.FolderRowView(model);
-
- // save new rowView to cache
- this.rowViews[model.get('id')] = rowView;
-
- this.$el.find('#first_folder_item').after(rowView.el);
-
- $('.library-row').hover(function() {
- $(this).find('.show_on_hover').show();
- }, function () {
- $(this).find('.show_on_hover').hide();
- });
- },
-
- /**
- * removes the view of the given model from the DOM
- * @param {Item or FolderAsModel} model of the view that will be removed
- */
- removeOne: function(model){
- this.$el.find('#' + model.id).remove();
- },
-
- /** Checks whether the list is empty and adds/removes the message */
- checkEmptiness : function(){
- if ((this.$el.find('.dataset_row').length === 0) && (this.$el.find('.folder_row').length === 0)){
- this.$el.find('.empty-folder-message').show();
- } else {
- this.$el.find('.empty-folder-message').hide();
- }
- },
-
- /** User clicked the table heading = he wants to sort stuff */
- sortColumnClicked : function(event){
- event.preventDefault();
- if (this.sort === 'asc'){
- this.sortFolder('name','desc');
- this.sort = 'desc';
- } else {
- this.sortFolder('name','asc');
- this.sort = 'asc';
- }
- this.render();
- this.renderAll();
- this.checkEmptiness();
- },
-
- /**
- * Sorts the underlying collection according to the parameters received.
- * Currently supports only sorting by name.
- */
- sortFolder: function(sort_by, order){
- if (sort_by === 'name'){
- if (order === 'asc'){
- return this.collection.sortByNameAsc();
- } else if (order === 'desc'){
- return this.collection.sortByNameDesc();
- }
- }
- },
-
- /**
- * User clicked the checkbox in the table heading
- * @param {context} event
- */
- selectAll : function (event) {
- var selected = event.target.checked;
- that = this;
- // Iterate each checkbox
- $(':checkbox', '#folder_list_body').each(function() {
- this.checked = selected;
- $row = $(this.parentElement.parentElement);
- // Change color of selected/unselected
- if (selected) {
- that.makeDarkRow($row);
- } else {
- that.makeWhiteRow($row);
- }
- });
- },
-
- /**
- * Check checkbox if user clicks on the whole row or
- * on the checkbox itself
- */
- selectClickedRow : function (event) {
- var checkbox = '';
- var $row;
- var source;
- if (event.target.localName === 'input'){
- checkbox = event.target;
- $row = $(event.target.parentElement.parentElement);
- source = 'input';
- } else if (event.target.localName === 'td') {
- checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0];
- $row = $(event.target.parentElement);
- source = 'td';
- }
- if (checkbox.checked){
- if (source==='td'){
- checkbox.checked = '';
- this.makeWhiteRow($row);
- } else if (source==='input') {
- this.makeDarkRow($row);
- }
- } else {
- if (source==='td'){
- checkbox.checked = 'selected';
- this.makeDarkRow($row);
- } else if (source==='input') {
- this.makeWhiteRow($row);
- }
- }
- },
-
- makeDarkRow: function($row){
- $row.removeClass('light').addClass('dark');
- $row.find('a').removeClass('light').addClass('dark');
- $row.find('.fa-file-o').removeClass('fa-file-o').addClass('fa-file');
- },
-
- makeWhiteRow: function($row){
- $row.removeClass('dark').addClass('light');
- $row.find('a').removeClass('dark').addClass('light');
- $row.find('.fa-file').removeClass('fa-file').addClass('fa-file-o');
- },
-
- templateFolder : function (){
- var tmpl_array = [];
-
- // BREADCRUMBS
- tmpl_array.push('<ol class="breadcrumb">');
- tmpl_array.push(' <li><a title="Return to the list of libraries" href="#">Libraries</a></li>');
- tmpl_array.push(' <% _.each(path, function(path_item) { %>');
- tmpl_array.push(' <% if (path_item[0] != id) { %>');
- tmpl_array.push(' <li><a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a></li> ');
- tmpl_array.push( '<% } else { %>');
- tmpl_array.push(' <li class="active"><span title="You are in this folder"><%- path_item[1] %></span></li>');
- tmpl_array.push(' <% } %>');
- tmpl_array.push(' <% }); %>');
- tmpl_array.push('</ol>');
-
- // FOLDER CONTENT
- tmpl_array.push('<table data-library-id="<%- parent_library_id %>" id="folder_table" class="grid table table-condensed">');
- tmpl_array.push(' <thead>');
- tmpl_array.push(' <th class="button_heading"></th>');
- tmpl_array.push(' <th style="text-align: center; width: 20px; " title="Check to select all datasets"><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>');
- tmpl_array.push(' <th><a class="sort-folder-link" title="Click to reverse order" href="#">name</a><span title="Sorted alphabetically" class="fa fa-sort-alpha-<%- order %>"></span></th>');
- tmpl_array.push(' <th style="width:5%;">data type</th>');
- tmpl_array.push(' <th style="width:10%;">size</th>');
- tmpl_array.push(' <th style="width:160px;">time updated (UTC)</th>');
- tmpl_array.push(' <th style="width:10%;"></th> ');
- tmpl_array.push(' </thead>');
- tmpl_array.push(' <tbody id="folder_list_body">');
- tmpl_array.push(' <tr id="first_folder_item">');
- tmpl_array.push(' <td><a href="#<% if (upper_folder_id !== 0){ print("folders/" + upper_folder_id)} %>" title="Go to parent folder" class="btn_open_folder btn btn-default btn-xs">..<a></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' <td></td>');
- tmpl_array.push(' </tr>');
-
- tmpl_array.push(' </tbody>');
- tmpl_array.push('</table>');
- tmpl_array.push('<div class="empty-folder-message" style="display:none;">This folder is either empty or you do not have proper access permissions to see the contents. If you expected something to show up please consult the <a href="https://wiki.galaxyproject.org/Admin/DataLibraries/LibrarySecurity" target="_blank">library security wikipage</a> or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.</div>');
-
- return _.template(tmpl_array.join(''));
- }
-
+
});
return {
diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js
--- a/client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js
+++ b/client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js
@@ -20,7 +20,9 @@
'click #toolbtn_bulk_import' : 'modalBulkImport',
'click #include_deleted_datasets_chk' : 'checkIncludeDeleted',
'click #toolbtn_show_libinfo' : 'showLibInfo',
- 'click #toolbtn_bulk_delete' : 'deleteSelectedDatasets'
+ 'click #toolbtn_bulk_delete' : 'deleteSelectedDatasets',
+ 'click #page_size_prompt' : 'showPageSizePrompt'
+
},
defaults: {
@@ -90,6 +92,22 @@
this.$el.html(toolbar_template(template_defaults));
},
+ /**
+ * Called from FolderListView when needed.
+ * @param {object} options common options
+ */
+ renderPaginator: function( options ){
+ this.options = _.extend( this.options, options );
+ var paginator_template = this.templatePaginator();
+ this.$el.find( '#folder_paginator' ).html( paginator_template({
+ id: this.options.id,
+ show_page: parseInt( this.options.show_page ),
+ page_count: parseInt( this.options.page_count ),
+ total_items_count: this.options.total_items_count,
+ items_shown: this.options.items_shown
+ }));
+ },
+
configureElements: function(options){
this.options = _.extend(this.options, options);
@@ -833,11 +851,11 @@
var popped_item = lddas_set.pop();
if ( typeof popped_item === "undefined" ) {
if ( this.options.chain_call_control.failed_number === 0 ){
- mod_toastr.success( 'Selected datasets deleted' );
+ mod_toastr.success( 'Selected datasets were deleted.' );
} else if ( this.options.chain_call_control.failed_number === this.options.chain_call_control.total_number ){
- mod_toastr.error( 'There was an error and no datasets were deleted.' );
+ mod_toastr.error( 'There was an error and no datasets were deleted. Please make sure you have sufficient permissions.' );
} else if ( this.options.chain_call_control.failed_number < this.options.chain_call_control.total_number ){
- mod_toastr.warning( 'Some of the datasets could not be deleted' );
+ mod_toastr.warning( 'Some of the datasets could not be deleted. Please make sure you have sufficient permissions.' );
}
Galaxy.modal.hide();
return this.deleted_lddas;
@@ -976,6 +994,14 @@
}
},
+ showPageSizePrompt: function(){
+ var folder_page_size = prompt( 'How many items per page do you want to see?', Galaxy.libraries.preferences.get( 'folder_page_size' ) );
+ if ( ( folder_page_size != null ) && ( folder_page_size == parseInt( folder_page_size ) ) ) {
+ Galaxy.libraries.preferences.set( { 'folder_page_size': parseInt( folder_page_size ) } );
+ Galaxy.libraries.folderListView.render( { id: this.options.id, show_page: 1 } );
+ }
+ },
+
templateToolBar: function(){
tmpl_array = [];
@@ -1016,7 +1042,6 @@
tmpl_array.push(' <button style="display:none;" data-toggle="tooltip" data-placement="top" title="Add Datasets to Current Folder" id="toolbtn_add_files" class="btn btn-default toolbtn_add_files primary-button add-library-items" type="button"><span class="fa fa-plus"></span><span class="fa fa-file"></span></span></button>');
tmpl_array.push('<% } %>');
-
tmpl_array.push(' <button data-toggle="tooltip" data-placement="top" title="Import selected datasets into history" id="toolbtn_bulk_import" class="primary-button dataset-manipulation" style="margin-left: 0.5em; display:none;" type="button"><span class="fa fa-book"></span> to History</button>');
tmpl_array.push(' <div id="toolbtn_dl" class="btn-group dataset-manipulation" style="margin-left: 0.5em; display:none; ">');
tmpl_array.push(' <button title="Download selected datasets as archive" id="drop_toggle" type="button" class="primary-button dropdown-toggle" data-toggle="dropdown">');
@@ -1032,6 +1057,10 @@
tmpl_array.push(' <button data-id="<%- id %>" data-toggle="tooltip" data-placement="top" title="Show library information" id="toolbtn_show_libinfo" class="primary-button" style="margin-left: 0.5em;" type="button"><span class="fa fa-info-circle"></span> Library Info</button>');
tmpl_array.push(' <span class="help-button" data-toggle="tooltip" data-placement="top" title="Visit Libraries Wiki"><a href="https://wiki.galaxyproject.org/DataLibraries/screen/FolderContents" target="_blank"><button class="primary-button" type="button"><span class="fa fa-question-circle"></span> Help</button></a></span>');
+ tmpl_array.push(' <span id="folder_paginator" class="library-paginator">');
+ // paginator will append here
+ tmpl_array.push(' </span>');
+
tmpl_array.push(' </div>');
// TOOLBAR END
tmpl_array.push(' <div id="folder_items_element">');
@@ -1239,7 +1268,41 @@
tmpl_array.push('</ul>');
return _.template(tmpl_array.join(''));
- }
+ },
+
+ templatePaginator: function(){
+ tmpl_array = [];
+
+ tmpl_array.push(' <ul class="pagination pagination-sm">');
+ tmpl_array.push(' <% if ( ( show_page - 1 ) > 0 ) { %>');
+ tmpl_array.push(' <% if ( ( show_page - 1 ) > page_count ) { %>'); // we are on higher page than total page count
+ tmpl_array.push(' <li><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>');
+ tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page - 1 ) %></a></li>');
+ tmpl_array.push(' <% } else { %>');
+ tmpl_array.push(' <li><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>');
+ tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( show_page - 1 ) %>"><% print( show_page - 1 ) %></a></li>');
+ tmpl_array.push(' <% } %>');
+ tmpl_array.push(' <% } else { %>'); // we are on the first page
+ tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>');
+ tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page - 1 ) %></a></li>');
+ tmpl_array.push(' <% } %>');
+ tmpl_array.push(' <li class="active">');
+ tmpl_array.push(' <a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page ) %></a>');
+ tmpl_array.push(' </li>');
+ tmpl_array.push(' <% if ( ( show_page ) < page_count ) { %>');
+ tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( show_page + 1 ) %>"><% print( show_page + 1 ) %></a></li>');
+ tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( page_count ) %>"><span class="fa fa-angle-double-right"></span></a></li>');
+ tmpl_array.push(' <% } else { %>');
+ tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page + 1 ) %></a></li>');
+ tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( page_count ) %>"><span class="fa fa-angle-double-right"></span></a></li>');
+ tmpl_array.push(' <% } %>');
+ tmpl_array.push(' </ul>');
+ tmpl_array.push(' <span>');
+ tmpl_array.push(' showing <a data-toggle="tooltip" data-placement="top" title="Click to change the number of items on page" id="page_size_prompt"><%- items_shown %></a> of <%- total_items_count %> items');
+ tmpl_array.push(' </span>');
+
+ return _.template(tmpl_array.join(''));
+ },
});
This diff is so big that we needed to truncate the remainder.
https://bitbucket.org/galaxy/galaxy-central/commits/a0bbd59cb420/
Changeset: a0bbd59cb420
User: abretaud
Date: 2014-12-11 16:02:05+00:00
Summary: fix method names and api doc
Affected #: 2 files
diff -r d76253bb22c4f64c73523956d119c0c5ab0a357b -r a0bbd59cb420442c2fb518540fd2fdfbb2b2da0a lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -211,11 +211,11 @@
self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd )
return self._loaded_content_version
- def _remove_entry(self, values, **kwd):
+ def _remove_entry(self, values):
raise NotImplementedError( "Abstract method" )
- def remove_entry(self, values, **kwd):
- self._remove_entry_and_reload( values,**kwd )
+ def remove_entry(self, values):
+ self._remove_entry( values )
return self._update_version()
def is_current_version( self, other_version ):
@@ -513,7 +513,7 @@
data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) )
return not is_error
- def _remove_entry_and_reload( self, values):
+ def _remove_entry( self, values):
# update every file
for filename in self.filenames:
diff -r d76253bb22c4f64c73523956d119c0c5ab0a357b -r a0bbd59cb420442c2fb518540fd2fdfbb2b2da0a lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -26,10 +26,10 @@
def delete( self, trans, id, **kwd ):
"""
DELETE /api/tool_data/{id}
- Removes a role from a group
+ Removes an item from a data table
:type id: str
- :param id: the encoded id of the history to delete
+ :param id: the id of the data table containing the item to delete
:type kwd: dict
:param kwd: (required) dictionary structure containing:
https://bitbucket.org/galaxy/galaxy-central/commits/6363c839528c/
Changeset: 6363c839528c
User: jmchilton
Date: 2014-12-15 05:17:31+00:00
Summary: Merged in abretaud/galaxy-central (pull request #577)
Add an API to remove items from tool data tables
Affected #: 2 files
diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/tools/data/__init__.py
--- a/lib/galaxy/tools/data/__init__.py
+++ b/lib/galaxy/tools/data/__init__.py
@@ -211,6 +211,13 @@
self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd )
return self._loaded_content_version
+ def _remove_entry(self, values):
+ raise NotImplementedError( "Abstract method" )
+
+ def remove_entry(self, values):
+ self._remove_entry( values )
+ return self._update_version()
+
def is_current_version( self, other_version ):
return self._loaded_content_version == other_version
@@ -506,6 +513,42 @@
data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) )
return not is_error
+ def _remove_entry( self, values):
+
+ # update every file
+ for filename in self.filenames:
+
+ if os.path.exists( filename ):
+ values = self._replace_field_separators( values )
+ self.filter_file_fields( filename, values )
+ else:
+ log.warn( "Cannot find index file '%s' for tool data table '%s'" % ( filename, self.name ) )
+
+ self.reload_from_files()
+
+ def filter_file_fields( self, loc_file, values ):
+ """
+ Reads separated lines from file and print back only the lines that pass a filter.
+ """
+ separator_char = (lambda c: '<TAB>' if c == '\t' else c)(self.separator)
+
+ with open(loc_file) as reader:
+ rval = ""
+ for i, line in enumerate( reader ):
+ if line.lstrip().startswith( self.comment_char ):
+ rval += line
+ else:
+ line_s = line.rstrip( "\n\r" )
+ if line_s:
+ fields = line_s.split( self.separator )
+ if fields != values:
+ rval += line
+
+ with open(loc_file, 'wb') as writer:
+ writer.write(rval)
+
+ return rval
+
def _replace_field_separators( self, fields, separator=None, replace=None, comment_char=None ):
#make sure none of the fields contain separator
#make sure separator replace is different from comment_char,
diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/webapps/galaxy/api/tool_data.py
--- a/lib/galaxy/webapps/galaxy/api/tool_data.py
+++ b/lib/galaxy/webapps/galaxy/api/tool_data.py
@@ -20,3 +20,44 @@
@web.expose_api
def show( self, trans, id, **kwds ):
return trans.app.tool_data_tables.data_tables[id].to_dict(view='element')
+
+ @web.require_admin
+ @web.expose_api
+ def delete( self, trans, id, **kwd ):
+ """
+ DELETE /api/tool_data/{id}
+ Removes an item from a data table
+
+ :type id: str
+ :param id: the id of the data table containing the item to delete
+ :type kwd: dict
+ :param kwd: (required) dictionary structure containing:
+
+ * payload: a dictionary itself containing:
+ * values: <TAB> separated list of column contents, there must be a value for all the columns of the data table
+ """
+ decoded_tool_data_id = id
+
+ try:
+ data_table = trans.app.tool_data_tables.data_tables.get(decoded_tool_data_id)
+ except:
+ data_table = None
+ if not data_table:
+ trans.response.status = 400
+ return "Invalid data table id ( %s ) specified." % str( decoded_tool_data_id )
+
+ values = None
+ if kwd.get( 'payload', None ):
+ values = kwd['payload'].get( 'values', '' )
+
+ if not values:
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified." % str( values )
+
+ split_values = values.split("\t")
+
+ if len(split_values) != len(data_table.get_column_name_list()):
+ trans.response.status = 400
+ return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list())))
+
+ return data_table.remove_entry(split_values)
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
0
commit/galaxy-central: jmchilton: Merged in lance_parsons/galaxy-central-pull-requests/blastdb_p.loc.sample (pull request #605)
by commits-noreply@bitbucket.org 14 Dec '14
by commits-noreply@bitbucket.org 14 Dec '14
14 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/ac537b0a4167/
Changeset: ac537b0a4167
User: jmchilton
Date: 2014-12-14 23:08:48+00:00
Summary: Merged in lance_parsons/galaxy-central-pull-requests/blastdb_p.loc.sample (pull request #605)
Removed blastdb_p.loc.sample, moved to tool-shed repo
Affected #: 1 file
diff -r 16e4b33e850e1a644624d97ecf676bbaaf1aabf9 -r ac537b0a4167cbd14e0cb93175cc85c096275697 tool-data/blastdb_p.loc.sample
--- a/tool-data/blastdb_p.loc.sample
+++ /dev/null
@@ -1,27 +0,0 @@
-#This is a sample file distributed with Galaxy that is used to define a
-#list of protein BLAST databases, using three columns tab separated
-#(longer whitespace are TAB characters):
-#
-#<unique_id><database_caption><base_name_path>
-#
-#The captions typically contain spaces and might end with the build date.
-#It is important that the actual database name does not have a space in it,
-#and that the first tab that appears in the line is right before the path.
-#
-#So, for example, if your database is NR and the path to your base name
-#is /data/blastdb/nr, then the blastdb_p.loc entry would look like this:
-#
-#nr NCBI NR (non redundant) /data/blastdb/nr
-#
-#and your /data/blastdb directory would contain all of the files associated
-#with the database, /data/blastdb/nr.*.
-#
-#Your blastdb_p.loc file should include an entry per line for each "base name"
-#you have stored. For example:
-#
-#nr_05Jun2010 NCBI NR (non redundant) 05 Jun 2010 /data/blastdb/05Jun2010/nr
-#nr_15Aug2010 NCBI NR (non redundant) 15 Aug 2010 /data/blastdb/15Aug2010/nr
-#...etc...
-#
-#See also blastdb.loc which is for any nucleotide BLAST database.
-#
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
0
2 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/6d6757abda20/
Changeset: 6d6757abda20
Branch: blastdb_p.loc.sample
User: lance_parsons
Date: 2014-12-10 17:43:33+00:00
Summary: Removed blastdb_p.loc.sample, moved to tool-shed repo
Affected #: 1 file
diff -r d66ad58a40e31defb6de0eb8b8b543b7e0f26ae5 -r 6d6757abda20cd49d85d8b59d382de9e69bf1588 tool-data/blastdb_p.loc.sample
--- a/tool-data/blastdb_p.loc.sample
+++ /dev/null
@@ -1,27 +0,0 @@
-#This is a sample file distributed with Galaxy that is used to define a
-#list of protein BLAST databases, using three columns tab separated
-#(longer whitespace are TAB characters):
-#
-#<unique_id><database_caption><base_name_path>
-#
-#The captions typically contain spaces and might end with the build date.
-#It is important that the actual database name does not have a space in it,
-#and that the first tab that appears in the line is right before the path.
-#
-#So, for example, if your database is NR and the path to your base name
-#is /data/blastdb/nr, then the blastdb_p.loc entry would look like this:
-#
-#nr NCBI NR (non redundant) /data/blastdb/nr
-#
-#and your /data/blastdb directory would contain all of the files associated
-#with the database, /data/blastdb/nr.*.
-#
-#Your blastdb_p.loc file should include an entry per line for each "base name"
-#you have stored. For example:
-#
-#nr_05Jun2010 NCBI NR (non redundant) 05 Jun 2010 /data/blastdb/05Jun2010/nr
-#nr_15Aug2010 NCBI NR (non redundant) 15 Aug 2010 /data/blastdb/15Aug2010/nr
-#...etc...
-#
-#See also blastdb.loc which is for any nucleotide BLAST database.
-#
https://bitbucket.org/galaxy/galaxy-central/commits/ac537b0a4167/
Changeset: ac537b0a4167
User: jmchilton
Date: 2014-12-14 23:08:48+00:00
Summary: Merged in lance_parsons/galaxy-central-pull-requests/blastdb_p.loc.sample (pull request #605)
Removed blastdb_p.loc.sample, moved to tool-shed repo
Affected #: 1 file
diff -r 16e4b33e850e1a644624d97ecf676bbaaf1aabf9 -r ac537b0a4167cbd14e0cb93175cc85c096275697 tool-data/blastdb_p.loc.sample
--- a/tool-data/blastdb_p.loc.sample
+++ /dev/null
@@ -1,27 +0,0 @@
-#This is a sample file distributed with Galaxy that is used to define a
-#list of protein BLAST databases, using three columns tab separated
-#(longer whitespace are TAB characters):
-#
-#<unique_id><database_caption><base_name_path>
-#
-#The captions typically contain spaces and might end with the build date.
-#It is important that the actual database name does not have a space in it,
-#and that the first tab that appears in the line is right before the path.
-#
-#So, for example, if your database is NR and the path to your base name
-#is /data/blastdb/nr, then the blastdb_p.loc entry would look like this:
-#
-#nr NCBI NR (non redundant) /data/blastdb/nr
-#
-#and your /data/blastdb directory would contain all of the files associated
-#with the database, /data/blastdb/nr.*.
-#
-#Your blastdb_p.loc file should include an entry per line for each "base name"
-#you have stored. For example:
-#
-#nr_05Jun2010 NCBI NR (non redundant) 05 Jun 2010 /data/blastdb/05Jun2010/nr
-#nr_15Aug2010 NCBI NR (non redundant) 15 Aug 2010 /data/blastdb/15Aug2010/nr
-#...etc...
-#
-#See also blastdb.loc which is for any nucleotide BLAST database.
-#
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
0
commit/galaxy-central: jmchilton: Allow specification of multiple datatypes_conf.xml files.
by commits-noreply@bitbucket.org 14 Dec '14
by commits-noreply@bitbucket.org 14 Dec '14
14 Dec '14
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/16e4b33e850e/
Changeset: 16e4b33e850e
User: jmchilton
Date: 2014-12-14 18:16:11+00:00
Summary: Allow specification of multiple datatypes_conf.xml files.
If there are conflicting datatype registrations - the registration that appears last in these files will be used. There is an open issue here regarding order of sniffers. This is going to just piggy back on whatever the tool shed would do - so however we decide for instance to resolve sniffer orders in that respect will work with this mechanism as well.
For now though - lets just specify that the best practice should be to just place all non-shed sniffers in one file if specifing multiple datatypes configurations manually like this.
Affected #: 2 files
diff -r 1422966f1ca88472b8685015f5a8cdd3dd0f1db9 -r 16e4b33e850e1a644624d97ecf676bbaaf1aabf9 config/galaxy.ini.sample
--- a/config/galaxy.ini.sample
+++ b/config/galaxy.ini.sample
@@ -212,8 +212,10 @@
# Directory where chrom len files are kept, currently mainly used by trackster
#len_file_path = tool-data/shared/ucsc/chrom
-# Datatypes config file, defines what data (file) types are available in
-# Galaxy (.sample is used if default does not exist).
+# Datatypes config file(s), defines what data (file) types are available in
+# Galaxy (.sample is used if default does not exist). If a datatype appears in
+# multiple files - the last definition is used (though the first sniffer is used
+# so limit sniffer definitions to one file).
#datatypes_config_file = config/datatypes_conf.xml
# Disable the 'Auto-detect' option for file uploads
diff -r 1422966f1ca88472b8685015f5a8cdd3dd0f1db9 -r 16e4b33e850e1a644624d97ecf676bbaaf1aabf9 lib/galaxy/config.py
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -583,8 +583,9 @@
for path in tool_configs:
if not os.path.exists( path ):
raise ConfigurationError("Tool config file not found: %s" % path )
- if not os.path.isfile( self.datatypes_config ):
- raise ConfigurationError("Datatypes config file not found: %s" % self.datatypes_config )
+ for datatypes_config in listify( self.datatypes_config ):
+ if not os.path.isfile( datatypes_config ):
+ raise ConfigurationError("Datatypes config file not found: %s" % datatypes_config )
# Check for deprecated options.
for key in self.config_dict.keys():
if key in self.deprecated_options:
@@ -746,7 +747,12 @@
# between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence.
installed_repository_manager.load_proprietary_datatypes()
# Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config.
- self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config )
+ datatypes_configs = self.config.datatypes_config
+ for datatypes_config in listify( datatypes_configs ):
+ # Setting override=False would make earlier files would take
+ # precedence - but then they wouldn't override tool shed
+ # datatypes.
+ self.datatypes_registry.load_datatypes( self.config.root, datatypes_config, override=True )
def _configure_object_store( self, **kwds ):
from galaxy.objectstore import build_object_store_from_config
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
0