diff --git a/client/galaxy/scripts/mvc/tool/tool-form-composite.js b/client/galaxy/scripts/mvc/tool/tool-form-composite.js index c54dd1ac328..3ce17a5595a 100644 --- a/client/galaxy/scripts/mvc/tool/tool-form-composite.js +++ b/client/galaxy/scripts/mvc/tool/tool-form-composite.js @@ -127,21 +127,24 @@ var View = Backbone.View.extend({ // identify and configure workflow parameters var wp_count = 0; this.wp_inputs = {}; + + function _ensureWorkflowParameter(wp_name) { + return self.wp_inputs[wp_name] = self.wp_inputs[wp_name] || { + label: wp_name, + name: wp_name, + type: "text", + color: `hsl( ${++wp_count * 100}, 70%, 30% )`, + style: "ui-form-wp-source", + links: [] + }; + } + function _handleWorkflowParameter(value, callback) { var re = /\$\{(.+?)\}/g; var match; while ((match = re.exec(String(value)))) { var wp_name = match[1]; - callback( - (self.wp_inputs[wp_name] = self.wp_inputs[wp_name] || { - label: wp_name, - name: wp_name, - type: "text", - color: `hsl( ${++wp_count * 100}, 70%, 30% )`, - style: "ui-form-wp-source", - links: [] - }) - ); + callback(_ensureWorkflowParameter(wp_name)); } } _.each(this.steps, (step, i) => { @@ -154,10 +157,8 @@ var View = Backbone.View.extend({ input.style = "ui-form-wp-target"; }); }); - _.each(step.post_job_actions, pja => { - _.each(pja.action_arguments, arg => { - _handleWorkflowParameter(arg, () => {}); - }); + _.each(step.replacement_parameters, wp_name => { + _ensureWorkflowParameter(wp_name); }); }); diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 27c13251e57..dbb0c7c2d3c 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -444,6 +444,7 @@ class WorkflowContentsManager(UsesAnnotations): step_model = { 'inputs' : [input.to_dict(trans) for input in inputs.values()] } + step_model['replacement_parameters'] = step.module.get_replacement_parameters(step) step_model['step_type'] = step.type step_model['step_label'] = step.label step_model['step_name'] = step.module.get_name() diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 437e8ef3cb3..f817b5d5705 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -2,6 +2,7 @@ Modules used in building workflows """ import logging +import re from json import loads from xml.etree.ElementTree import ( Element, @@ -259,6 +260,11 @@ class WorkflowModule(object): progress.set_step_outputs(invocation_step, outputs, already_persisted=True) + def get_replacement_parameters(self, step): + """Return a list of replacement parameters.""" + + return [] + class SubWorkflowModule(WorkflowModule): # Two step improvements to build runtime inputs for subworkflow modules @@ -403,6 +409,16 @@ class SubWorkflowModule(WorkflowModule): return inputs + def get_replacement_parameters(self, step): + """Return a list of replacement parameters.""" + replacement_parameters = set() + for subworkflow_step in self.subworkflow.steps: + module = subworkflow_step.module + for replacement_parameter in module.get_replacement_parameters(subworkflow_step): + replacement_parameters.add(replacement_parameter) + + return list(replacement_parameters) + class InputProxy(object): """Provide InputParameter-interfaces over inputs but renamed for workflow context.""" @@ -1088,6 +1104,16 @@ class ToolModule(WorkflowModule): action_arguments = None return PostJobAction(value['action_type'], step, output_name, action_arguments) + def get_replacement_parameters(self, step): + """Return a list of replacement parameters.""" + replacement_parameters = set() + for pja in step.post_job_actions: + for argument in pja.action_arguments.values(): + for match in re.findall(r'\$\{(.+?)\}', argument): + replacement_parameters.add(match) + + return list(replacement_parameters) + class WorkflowModuleFactory(object): diff --git a/test/api/test_workflows.py b/test/api/test_workflows.py index 8d66faa328f..077e5431a35 100644 --- a/test/api/test_workflows.py +++ b/test/api/test_workflows.py @@ -20,8 +20,10 @@ from base.populators import ( # noqa: I100 WorkflowPopulator ) from base.workflow_fixtures import ( # noqa: I100 + WORKFLOW_NESTED_REPLACEMENT_PARAMETER, WORKFLOW_NESTED_RUNTIME_PARAMETER, WORKFLOW_NESTED_SIMPLE, + WORKFLOW_RENAME_ON_INPUT, WORKFLOW_RUNTIME_PARAMETER_AFTER_PAUSE, WORKFLOW_WITH_DYNAMIC_OUTPUT_COLLECTION, WORKFLOW_WITH_OUTPUT_COLLECTION, @@ -224,6 +226,7 @@ class BaseWorkflowsApiTestCase(api.ApiTestCase): test_data = jobs_descriptions.get("test_data", {}) parameters = test_data.pop('step_parameters', {}) + replacement_parameters = test_data.pop("replacement_parameters", {}) inputs, label_map, has_uploads = load_data_dict(history_id, test_data, self.dataset_populator, self.dataset_collection_populator) workflow_request = dict( history="hist_id=%s" % history_id, @@ -234,6 +237,8 @@ class BaseWorkflowsApiTestCase(api.ApiTestCase): if parameters: workflow_request["parameters"] = dumps(parameters) workflow_request["parameters_normalized"] = True + if replacement_parameters: + workflow_request["replacement_params"] = dumps(replacement_parameters) if has_uploads: self.dataset_populator.wait_for_history(history_id, assert_ok=True) url = "workflows/%s/usage" % (workflow_id) @@ -980,6 +985,23 @@ test_data: content = self.dataset_populator.get_history_dataset_content(history_id) assert len([x for x in content.split("\n") if x]) == 2 + @skip_without_tool("cat") + def test_run_subworkflow_replacment_parameters(self): + with self.dataset_populator.test_history() as history_id: + workflow_run_description = """%s + +test_data: + replacement_parameters: + replaceme: moocow + outer_input: + value: 1.bed + type: File +""" % WORKFLOW_NESTED_REPLACEMENT_PARAMETER + self._run_jobs(workflow_run_description, history_id=history_id) + + details = self.dataset_populator.get_history_dataset_details(history_id) + assert details["name"] == "moocow suffix" + @skip_without_tool("random_lines1") def test_run_runtime_parameters_after_pause(self): with self.dataset_populator.test_history() as history_id: @@ -2247,25 +2269,7 @@ test_data: {} @skip_without_tool("cat") def test_run_rename_based_on_input(self): with self.dataset_populator.test_history() as history_id: - self._run_jobs(""" -class: GalaxyWorkflow -inputs: - - id: input1 -steps: - - tool_id: cat - label: first_cat - state: - input1: - $link: input1 - outputs: - out_file1: - rename: "#{input1 | basename} suffix" -test_data: - input1: - value: 1.fasta - type: File - name: fasta1 -""", history_id=history_id) + self._run_jobs(WORKFLOW_RENAME_ON_INPUT, history_id=history_id) content = self.dataset_populator.get_history_dataset_details(history_id, wait=True, assert_ok=True) name = content["name"] assert name == "fasta1 suffix", name diff --git a/test/base/workflow_fixtures.py b/test/base/workflow_fixtures.py index ac23abddea9..38591237615 100644 --- a/test/base/workflow_fixtures.py +++ b/test/base/workflow_fixtures.py @@ -319,3 +319,67 @@ steps: seed_source_selector: set_seed seed: asdf """ + +WORKFLOW_RENAME_ON_INPUT = """ +class: GalaxyWorkflow +inputs: + - id: input1 +steps: + - tool_id: cat + label: first_cat + state: + input1: + $link: input1 + outputs: + out_file1: + rename: "#{input1 | basename} suffix" +test_data: + input1: + value: 1.fasta + type: File + name: fasta1 +""" + +WORKFLOW_RENAME_ON_REPLACEMENT_PARAM = """ +class: GalaxyWorkflow +inputs: + - id: input1 +steps: + - tool_id: cat + label: first_cat + state: + input1: + $link: input1 + outputs: + out_file1: + rename: "${replaceme} suffix" +""" + +WORKFLOW_NESTED_REPLACEMENT_PARAMETER = """ +class: GalaxyWorkflow +inputs: + - id: outer_input +outputs: + - id: outer_output + source: nested_workflow#workflow_output +steps: + - run: + class: GalaxyWorkflow + inputs: + - id: inner_input + outputs: + - id: workflow_output + source: first_cat#out_file1 + steps: + - tool_id: cat + label: first_cat + state: + input1: + $link: inner_input + outputs: + out_file1: + rename: "${replaceme} suffix" + label: nested_workflow + connect: + inner_input: outer_input +""" diff --git a/test/selenium_tests/test_workflow_run.py b/test/selenium_tests/test_workflow_run.py index 3f786774b6a..60f9913fdc3 100644 --- a/test/selenium_tests/test_workflow_run.py +++ b/test/selenium_tests/test_workflow_run.py @@ -2,8 +2,10 @@ import yaml from base import rules_test_data from base.populators import load_data_dict from base.workflow_fixtures import ( + WORKFLOW_NESTED_REPLACEMENT_PARAMETER, WORKFLOW_NESTED_RUNTIME_PARAMETER, WORKFLOW_NESTED_SIMPLE, + WORKFLOW_RENAME_ON_REPLACEMENT_PARAM, WORKFLOW_RUNTIME_PARAMETER_SIMPLE, WORKFLOW_SIMPLE_CAT_TWICE, WORKFLOW_WITH_DYNAMIC_OUTPUT_COLLECTION, @@ -78,6 +80,38 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions): self._assert_has_3_lines_after_run(hid=2) + @selenium_test + @managed_history + def test_replacement_parameters(self): + self.perform_upload(self.get_filename("1.txt")) + self.wait_for_history() + self.open_in_workflow_run(WORKFLOW_RENAME_ON_REPLACEMENT_PARAM) + self.screenshot("workflow_run_rename_simple_empty") + self._set_replacement_parameter("replaceme", "moocow") + self.screenshot("workflow_run_rename_simple_input") + self.workflow_run_submit() + output_hid = 2 + self.history_panel_wait_for_hid_ok(output_hid, allowed_force_refreshes=1) + history_id = self.current_history_id() + details = self.dataset_populator.get_history_dataset_details(history_id, hid=output_hid) + assert details["name"] == "moocow suffix", details + + @selenium_test + @managed_history + def test_replacement_parameters_on_subworkflows(self): + self.perform_upload(self.get_filename("1.txt")) + self.wait_for_history() + self.open_in_workflow_run(WORKFLOW_NESTED_REPLACEMENT_PARAMETER) + self.screenshot("workflow_run_rename_subworkflow_empty") + self._set_replacement_parameter("replaceme", "moocow") + self.screenshot("workflow_run_rename_subworkflow_input") + self.workflow_run_submit() + output_hid = 2 + self.history_panel_wait_for_hid_ok(output_hid, allowed_force_refreshes=1) + history_id = self.current_history_id() + details = self.dataset_populator.get_history_dataset_details(history_id, hid=output_hid) + assert details["name"] == "moocow suffix", details + @selenium_test def test_execution_with_tool_upgrade(self): name = self.workflow_upload_yaml_with_random_name(WORKFLOW_WITH_OLD_TOOL_VERSION, exact_tools=True) @@ -148,3 +182,12 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions): assert initial_value == "1", initial_value input_element.clear() input_element.send_keys("3") + + def _set_replacement_parameter(self, tour_id, value): + # for random_lines num_lines parameter as runtime parameter in workflow form. + div = self.tool_parameter_div(tour_id) + input_element = div.find_element_by_css_selector("input") + initial_value = input_element.get_attribute("value") + assert initial_value == "", initial_value + input_element.clear() + input_element.send_keys(value)