Merge pull request #6925 from jmchilton/workflow_step_parameters_3

Add workflow step parameters to workflow editor (try 3)
This commit is contained in:
Marius van den Beek
2018-11-10 20:45:32 +01:00
committed by GitHub
23 changed files with 471 additions and 144 deletions
+3 -1
View File
@@ -84,7 +84,9 @@ export var Manager = Backbone.Model.extend({
if (field && field.value) {
value = field.value();
if (input.ignore === undefined || input.ignore != value) {
if (field.collapsed && input.collapsible_value) {
if (field.collapsed && field.connected) {
value = { __class__: "ConnectedValue" };
} else if (field.collapsed && input.collapsible_value) {
value = input.collapsible_value;
}
add(flat_id, input.id, value);
+53 -12
View File
@@ -25,6 +25,7 @@ export default Backbone.View.extend({
this.$collapsible = this.$(".ui-form-collapsible");
this.$collapsible_text = this.$(".ui-form-collapsible-text");
this.$collapsible_icon = this.$(".ui-form-collapsible-icon");
this.$connected_icon = this.$(".ui-form-connected-icon");
this.$title = this.$(".ui-form-title");
this.$title_text = this.$(".ui-form-title-text");
this.$error_text = this.$(".ui-form-error-text");
@@ -36,16 +37,35 @@ export default Backbone.View.extend({
// decide wether to expand or collapse fields
var collapsible_value = this.model.get("collapsible_value");
var value = JSON.stringify(this.model.get("value"));
var connected = value == JSON.stringify({ __class__: "ConnectedValue" });
this.field.connected = connected;
this.field.collapsed =
collapsible_value !== undefined &&
JSON.stringify(this.model.get("value")) == JSON.stringify(collapsible_value);
this.field.connected ||
(collapsible_value !== undefined &&
JSON.stringify(this.model.get("value")) == JSON.stringify(collapsible_value));
this.listenTo(this.model, "change", this.render, this);
this.render();
// add click handler
var self = this;
this.$collapsible.on("click", () => {
this.$collapsible_icon.on("click", () => {
if (self.field.connected) {
return;
}
self.field.collapsed = !self.field.collapsed;
if (self.field.collapsed) {
self.field.connected = false;
}
app.trigger && app.trigger("change");
self.render();
});
this.$connected_icon.on("click", () => {
self.field.connected = !self.field.connected;
self.field.collapsed = self.field.connected;
if (!self.field.connected) {
this.model.set("value", null);
}
app.trigger && app.trigger("change");
self.render();
});
@@ -108,21 +128,41 @@ export default Backbone.View.extend({
style: this.model.get("style")
});
// render collapsible options
if (
const connected = this.field.connected;
const collapsible =
!connected &&
!this.field.collapsible_disabled &&
!this.model.get("disabled") &&
this.model.get("collapsible_value") !== undefined
) {
this.model.get("collapsible_value") !== undefined;
const connectable = this.model.get("connectable");
if (collapsible || connectable) {
var collapsible_state = this.field.collapsed ? "enable" : "disable";
this.$title_text.hide();
this.$collapsible.show();
this.$collapsible_text.text(this.model.get("label"));
this.$collapsible_icon
.removeClass()
.addClass("icon")
.addClass(this.model.get(`cls_${collapsible_state}`))
.attr("data-original-title", this.model.get(`text_${collapsible_state}`))
.tooltip({ placement: "bottom" });
if (collapsible && !connected) {
this.$collapsible_icon
.removeClass()
.addClass("icon ui-form-collapsible-icon")
.addClass(this.model.get(`cls_${collapsible_state}`))
.attr("data-original-title", this.model.get(`text_${collapsible_state}`))
.tooltip({ placement: "bottom" })
.show();
} else {
this.$collapsible_icon.hide();
}
if (connectable) {
const connectedIconStyle = this.field.connected ? "fa fa-times" : "fa fa-arrows-h";
this.$connected_icon
.removeClass()
.addClass("icon ui-form-connected-icon")
.addClass(connectedIconStyle)
.attr("data-original-title", this.model.get(`text_${collapsible_state}`))
.tooltip({ placement: "bottom" })
.show();
} else {
this.$connected_icon.hide();
}
} else {
this.$title_text.show().text(this.model.get("label"));
this.$collapsible.hide();
@@ -145,6 +185,7 @@ export default Backbone.View.extend({
$("<div/>")
.addClass("ui-form-collapsible")
.append($("<i/>").addClass("ui-form-collapsible-icon"))
.append($("<i/>").addClass("ui-form-connected-icon"))
.append($("<span/>").addClass("ui-form-collapsible-text"))
)
.append($("<span/>").addClass("ui-form-title-text"))
@@ -175,6 +175,7 @@ var View = Backbone.View.extend({
text_value: input_def.text_value,
collapsible_value: input_def.collapsible_value,
collapsible_preview: input_def.collapsible_preview,
connectable: input_def.connectable,
help: input_def.help,
argument: input_def.argument,
disabled: input_def.disabled,
@@ -90,6 +90,7 @@ var Tool = Backbone.View.extend({
var options = form.model.attributes;
Utils.deepeach(options.inputs, input => {
if (input.type) {
input.connectable = true;
if (["data", "data_collection"].indexOf(input.type) != -1) {
input.type = "hidden";
input.info = `Data input '${input.name}' (${Utils.textify(input.extensions)})`;
@@ -105,6 +106,7 @@ var Tool = Backbone.View.extend({
});
Utils.deepeach(options.inputs, input => {
if (input.type === "conditional") {
input.connectable = false;
input.test_param.collapsible_value = undefined;
}
});
@@ -3,5 +3,6 @@ export default {
data_input: "fa-file-o",
data_collection_input: "fa-folder-o",
subworkflow: "fa-sitemap fa-rotate-270",
parameter_input: "fa-pencil",
pause: "fa-pause"
};
@@ -6,6 +6,14 @@ import NodeView from "mvc/workflow/workflow-view-node";
/* global $ */
/* global Galaxy */
var StepParameterTypes = [
'text',
'integer',
'float',
'boolean',
'color',
]
var Node = Backbone.Model.extend({
initialize: function(app, attr) {
this.app = app;
@@ -192,6 +200,7 @@ var Node = Backbone.Model.extend({
this.content_id = this.config_form.id;
}
},
init_field_data: function(data) {
//console.debug("init_field_data: ", data);
if (data.type) {
@@ -214,13 +223,14 @@ var Node = Backbone.Model.extend({
node: node
});
node.nodeView = nodeView;
$.each(data.data_inputs, (i, input) => {
$.each(data.inputs, (i, input) => {
nodeView.addDataInput(input);
});
if (data.data_inputs.length > 0 && data.data_outputs.length > 0) {
if (data.inputs.length > 0 && data.outputs.length > 0) {
nodeView.addRule();
}
$.each(data.data_outputs, (i, output) => {
$.each(data.outputs, (i, output) => {
nodeView.addDataOutput(output);
});
nodeView.render();
@@ -229,7 +239,7 @@ var Node = Backbone.Model.extend({
update_field_data: function(data) {
var node = this;
var nodeView = node.nodeView;
// remove unused output views and remove pre-existing output views from data.data_outputs,
// remove unused output views and remove pre-existing output views from data.outputs,
// so that these are not added twice.
var unused_outputs = [];
// nodeView.outputViews contains pre-existing outputs,
@@ -237,7 +247,7 @@ var Node = Backbone.Model.extend({
// Now we gather the unused outputs
$.each(nodeView.outputViews, (i, output_view) => {
var cur_name = output_view.output.name;
var data_names = data.data_outputs;
var data_names = data.outputs;
var cur_name_in_data_outputs = false;
_.each(data_names, data_name => {
if (data_name.name == cur_name) {
@@ -265,7 +275,7 @@ var Node = Backbone.Model.extend({
node.workflow_outputs.splice(i, 1); // removes output from list of workflow outputs
}
});
$.each(data.data_outputs, (i, output) => {
$.each(data.outputs, (i, output) => {
if (!nodeView.outputViews[output.name]) {
nodeView.addDataOutput(output); // add data output if it does not yet exist
} else {
@@ -291,7 +301,7 @@ var Node = Backbone.Model.extend({
var old_body = nodeView.$("div.inputs");
var new_body = nodeView.newInputsDiv();
var newTerminalViews = {};
_.each(data.data_inputs, input => {
_.each(data.inputs, input => {
var terminalView = node.nodeView.addDataInput(input, new_body);
newTerminalViews[input.name] = terminalView;
});
@@ -305,8 +315,9 @@ var Node = Backbone.Model.extend({
// type (not really valid right?) but adding special logic here for
// data collection input parameters that can have their collection
// change.
if (data.data_outputs.length == 1 && "collection_type" in data.data_outputs[0]) {
nodeView.updateDataOutput(data.data_outputs[0]);
var data_outputs = data.outputs;
if (data_outputs.length == 1 && "collection_type" in data_outputs[0]) {
nodeView.updateDataOutput(data_outputs[0]);
}
old_body.replaceWith(new_body);
if ("workflow_outputs" in data) {
@@ -431,6 +431,18 @@ var InputTerminal = BaseInputTerminal.extend({
}
});
var InputParameterTerminal = BaseInputTerminal.extend({
update: function(input) {
this.type = input.type;
},
connect: function(connector) {
BaseInputTerminal.prototype.connect.call(this, connector);
},
attachable: function(other) {
return this.type == other.attributes.type;
},
});
var InputCollectionTerminal = BaseInputTerminal.extend({
update: function(input) {
this.multiple = false;
@@ -566,9 +578,14 @@ var OutputCollectionTerminal = Terminal.extend({
}
});
var OutputParameterTerminal = Terminal.extend({
});
export default {
InputTerminal: InputTerminal,
InputParameterTerminal: InputParameterTerminal,
OutputTerminal: OutputTerminal,
OutputParameterTerminal: OutputParameterTerminal,
InputCollectionTerminal: InputCollectionTerminal,
OutputCollectionTerminal: OutputCollectionTerminal,
TerminalMapping: TerminalMapping,
@@ -9,7 +9,7 @@ var DataInputView = Backbone.View.extend({
this.nodeView = options.nodeView;
this.terminalElement = options.terminalElement;
this.$el.attr("name", this.input.name).html(this.input.label);
this.$el.attr("name", this.input.name).html(this.input.label || this.input.name);
if (!options.skipResize) {
this.$el.css({
@@ -82,6 +82,61 @@ var DataOutputView = Backbone.View.extend({
}
});
var ParameterOutputView = Backbone.View.extend({
className: "form-row dataRow",
initialize: function(options) {
this.output = options.output;
this.terminalElement = options.terminalElement;
this.nodeView = options.nodeView;
var output = this.output;
var label = output.label || output.name;
var node = this.nodeView.node;
this.$el.html(label);
this.calloutView = null;
if (["tool", "subworkflow"].indexOf(node.type) >= 0) {
var calloutView = new OutputCalloutView({
label: label,
output: output,
node: node
});
this.calloutView = calloutView;
this.$el.append(calloutView.el);
this.$el.hover(
() => {
calloutView.hoverImage();
},
() => {
calloutView.resetImage();
}
);
}
this.$el.css({
position: "absolute",
left: -1000,
top: -1000,
display: "none"
});
$("body").append(this.el);
this.nodeView.updateMaxWidth(this.$el.outerWidth() + 17);
this.$el
.css({
position: "",
left: "",
top: "",
display: ""
})
.detach();
},
redrawWorkflowOutput: function() {
if (this.calloutView) {
this.calloutView.resetImage();
}
}
});
var OutputCalloutView = Backbone.View.extend({
tagName: "div",
@@ -133,5 +188,6 @@ var OutputCalloutView = Backbone.View.extend({
export default {
DataInputView: DataInputView,
DataOutputView: DataOutputView
DataOutputView: DataOutputView,
ParameterOutputView: ParameterOutputView,
};
@@ -48,10 +48,12 @@ export default Backbone.View.extend({
skipResize = false;
}
var terminalView = this.terminalViews[input.name];
var terminalViewClass =
input.input_type == "dataset_collection"
? TerminalViews.InputCollectionTerminalView
: TerminalViews.InputTerminalView;
var terminalViewClass = TerminalViews.InputTerminalView;
if (input.input_type == "dataset_collection") {
terminalViewClass = TerminalViews.InputCollectionTerminalView;
} else if (input.input_type == "parameter") {
terminalViewClass = TerminalViews.InputParameterTerminalView;
}
if (terminalView && !(terminalView instanceof terminalViewClass)) {
terminalView.el.terminal.destroy();
terminalView = null;
@@ -80,14 +82,19 @@ export default Backbone.View.extend({
},
addDataOutput: function(output) {
var terminalViewClass = output.collection
? TerminalViews.OutputCollectionTerminalView
: TerminalViews.OutputTerminalView;
var terminalViewClass = TerminalViews.OutputTerminalView;
var outputViewClass = DataViews.DataOutputView;
if (output.collection) {
terminalViewClass = TerminalViews.OutputCollectionTerminalView;
} else if (output.parameter) {
terminalViewClass = TerminalViews.OutputParameterTerminalView;
outputViewClass = DataViews.ParameterOutputView;
}
var terminalView = new terminalViewClass({
node: this.node,
output: output
});
var outputView = new DataViews.DataOutputView({
var outputView = new outputViewClass({
output: output,
terminalElement: terminalView.el,
nodeView: this
@@ -151,6 +151,17 @@ var InputTerminalView = BaseInputTerminalView.extend({
}
});
var InputParameterTerminalView = BaseInputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: InputTerminalMappingView,
terminalForInput: function(input) {
return new Terminals.InputParameterTerminal({
element: this.el,
input: input
});
}
});
var InputCollectionTerminalView = BaseInputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: InputTerminalMappingView,
@@ -269,9 +280,25 @@ var OutputCollectionTerminalView = BaseOutputTerminalView.extend({
}
});
var OutputParameterTerminalView = BaseOutputTerminalView.extend({
terminalMappingClass: Terminals.TerminalMapping,
terminalMappingViewClass: TerminalMappingView,
terminalForOutput: function(output) {
var collection_type = output.collection_type;
var collection_type_source = output.collection_type_source;
var terminal = new Terminals.OutputCollectionTerminal({
element: this.el,
type: output.type,
});
return terminal;
}
});
export default {
InputTerminalView: InputTerminalView,
InputParameterTerminalView: InputParameterTerminalView,
OutputTerminalView: OutputTerminalView,
OutputParameterTerminalView: OutputParameterTerminalView,
InputCollectionTerminalView: InputCollectionTerminalView,
OutputCollectionTerminalView: OutputCollectionTerminalView
};
@@ -357,16 +357,16 @@ QUnit.module("Node unit test", {
},
init_field_data_simple: function(option_overrides) {
var data = Utils.merge(option_overrides, {
data_inputs: [{ name: "input1", extensions: ["data"] }],
data_outputs: [{ name: "output1", extensions: ["data"] }],
inputs: [{ name: "input1", extensions: ["data"] }],
outputs: [{ name: "output1", extensions: ["data"] }],
label: null
});
this.node.init_field_data(data);
},
update_field_data_with_new_input: function(option_overrides) {
var new_data = Utils.merge(option_overrides, {
data_inputs: [{ name: "input1", extensions: ["data"] }, { name: "extra_0|input1", extensions: ["data"] }],
data_outputs: [{ name: "output1", extensions: ["data"] }],
inputs: [{ name: "input1", extensions: ["data"] }, { name: "extra_0|input1", extensions: ["data"] }],
outputs: [{ name: "output1", extensions: ["data"] }],
post_job_actions: "{}",
label: "New Label"
});
@@ -401,8 +401,8 @@ QUnit.test("init_field_data properties", function(assert) {
var node = this.node;
this.expect_workflow_node_changed(assert, function() {
var data = {
data_inputs: [],
data_outputs: [],
inputs: [],
outputs: [],
type: "tool",
name: "cat1",
config_form: "{}",
@@ -507,8 +507,8 @@ QUnit.test("update_field_data destroys old terminals", function(assert) {
var node = this.node;
this.expect_workflow_node_changed(assert, function() {
var data = {
data_inputs: [{ name: "input1", extensions: ["data"] }, { name: "willDisappear", extensions: ["data"] }],
data_outputs: [{ name: "output1", extensions: ["data"] }]
inputs: [{ name: "input1", extensions: ["data"] }, { name: "willDisappear", extensions: ["data"] }],
outputs: [{ name: "output1", extensions: ["data"] }]
};
node.init_field_data(data);
var old_input_terminal = node.input_terminals.willDisappear;
+7 -10
View File
@@ -561,7 +561,7 @@ class WorkflowContentsManager(UsesAnnotations):
data['upgrade_messages'][step.order_index] = {module.tool.name: "\n".join(module.version_changes)}
# Get user annotation.
annotation_str = self.get_item_annotation_str(trans.sa_session, trans.user, step) or ''
config_form = module.get_config_form()
config_form = module.get_config_form(step=step)
# Pack attributes into plain dictionary
step_dict = {
'id': step.order_index,
@@ -571,8 +571,8 @@ class WorkflowContentsManager(UsesAnnotations):
'name': module.get_name(),
'tool_state': module.get_state(),
'errors': module.get_errors(),
'data_inputs': module.get_data_inputs(),
'data_outputs': module.get_data_outputs(),
'inputs': module.get_all_inputs(connectable_only=True),
'outputs': module.get_all_outputs(),
'config_form': config_form,
'annotation': annotation_str,
'post_job_actions': {},
@@ -598,9 +598,6 @@ class WorkflowContentsManager(UsesAnnotations):
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:
@@ -653,12 +650,12 @@ class WorkflowContentsManager(UsesAnnotations):
"""
for order_index in sorted(steps):
step = steps[order_index]
for i, step_data_output in enumerate(step['data_outputs']):
for i, step_data_output in enumerate(step['outputs']):
if step_data_output.get('collection_type_source') and step_data_output['collection_type'] is None:
collection_type_source = step_data_output['collection_type_source']
for input_connection in step['input_connections'].get(collection_type_source, []):
input_step = steps[input_connection['id']]
for input_step_data_output in input_step['data_outputs']:
for input_step_data_output in input_step['outputs']:
if input_step_data_output['name'] == input_connection['output_name']:
step_data_output['collection_type'] = input_step_data_output.get('collection_type')
return steps
@@ -789,8 +786,6 @@ class WorkflowContentsManager(UsesAnnotations):
# If the tool is installed we attempt to verify input values
# and connections, otherwise the last known state will be dumped without modifications.
visit_input_values(module.tool.inputs, module.state.inputs, callback)
# 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 or conn.non_data_connection)]
# Encode input connections as dictionary
input_conn_dict = {}
@@ -846,6 +841,8 @@ class WorkflowContentsManager(UsesAnnotations):
label = "Input Dataset"
elif step_type == "data_collection_input":
label = "Input Dataset Collection"
elif step_type == 'parameter_input':
label = "Input Parameter"
else:
raise ValueError("Invalid step_type %s" % step_type)
if legacy:
+1 -6
View File
@@ -4192,14 +4192,9 @@ class WorkflowStepConnection(RepresentById):
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)
return (self.output_name == self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION)
def copy(self):
# TODO: handle subworkflow ids...
+3 -6
View File
@@ -9,7 +9,7 @@ from boltons.iterutils import remap
from galaxy.util.expressions import ExpressionContext
from galaxy.util.json import safe_loads
from .basic import DataCollectionToolParameter, DataToolParameter, RuntimeValue, SelectToolParameter
from .basic import DataCollectionToolParameter, DataToolParameter, is_runtime_value, runtime_to_json, SelectToolParameter
from .grouping import Conditional, Repeat, Section, UploadDataset
REPLACE_ON_TRUTHY = object()
@@ -179,11 +179,8 @@ def check_param(trans, param, incoming_value, param_values):
error = None
try:
if trans.workflow_building_mode:
if isinstance(value, RuntimeValue):
return [{'__class__' : 'RuntimeValue'}, None]
if isinstance(value, dict):
if value.get('__class__') == 'RuntimeValue':
return [value, None]
if is_runtime_value(value):
return [runtime_to_json(value), None]
value = param.from_json(value, trans, param_values)
param.validate(value, trans)
except ValueError as e:
+25 -4
View File
@@ -56,7 +56,7 @@ def contains_workflow_parameter(value, search=False):
def is_runtime_value(value):
return isinstance(value, RuntimeValue) or (isinstance(value, dict)
and value.get("__class__") == "RuntimeValue")
and value.get("__class__") in ["RuntimeValue", "ConnectedValue"])
def is_runtime_context(trans, other_values):
@@ -155,13 +155,13 @@ class ToolParameter(Dictifiable):
def value_to_basic(self, value, app, use_security=False):
if is_runtime_value(value):
return {'__class__': 'RuntimeValue'}
return runtime_to_json(value)
return self.to_json(value, app, use_security)
def value_from_basic(self, value, app, ignore_errors=False):
# Handle Runtime and Unvalidated values
if is_runtime_value(value):
return RuntimeValue()
return runtime_to_object(value)
elif isinstance(value, dict) and value.get('__class__') == 'UnvalidatedValue':
return value['value']
# Delegate to the 'to_python' method
@@ -2246,7 +2246,7 @@ class BaseJsonToolParameter(ToolParameter):
def value_to_basic(self, value, app, use_security=False):
if is_runtime_value(value):
return {'__class__': 'RuntimeValue'}
return runtime_to_json(value)
return value
def to_json(self, value, app, use_security):
@@ -2323,8 +2323,29 @@ parameter_types = dict(
)
def runtime_to_json(runtime_value):
if isinstance(runtime_value, ConnectedValue) or (isinstance(runtime_value, dict) and runtime_value["__class__"] == "ConnectedValue"):
return {"__class__": "ConnectedValue"}
else:
return {"__class__": "RuntimeValue"}
def runtime_to_object(runtime_value):
if isinstance(runtime_value, ConnectedValue) or (isinstance(runtime_value, dict) and runtime_value["__class__"] == "ConnectedValue"):
return ConnectedValue()
else:
return RuntimeValue()
class RuntimeValue(object):
"""
Wrapper to note a value that is not yet set, but will be required at runtime.
"""
pass
class ConnectedValue(RuntimeValue):
"""
Wrapper to note a value that is not yet set, but will be inferred from a connection.
"""
pass
+2 -2
View File
@@ -557,8 +557,8 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn
'annotation' : inputs.get('__annotation', ''),
'name' : module.get_name(),
'tool_state' : module.get_state(),
'data_inputs' : module.get_data_inputs(),
'data_outputs' : module.get_data_outputs(),
'inputs' : module.get_all_inputs(connectable_only=True),
'outputs' : module.get_all_outputs(),
'config_form' : module.get_config_form(),
'post_job_actions' : module.get_post_job_actions(inputs)
}
+77 -50
View File
@@ -30,11 +30,12 @@ from galaxy.tools.parameters import (
)
from galaxy.tools.parameters.basic import (
BooleanToolParameter,
ConnectedValue,
DataCollectionToolParameter,
DataToolParameter,
is_runtime_value,
parameter_types,
RuntimeValue,
runtime_to_json,
SelectToolParameter,
TextToolParameter,
workflow_building_modes
@@ -151,12 +152,18 @@ class WorkflowModule(object):
""" This returns inputs displayed in the workflow editor """
return {}
def get_all_inputs(self, data_only=False, connectable_only=False):
return []
def get_data_inputs(self):
""" Get configure time data input descriptions. """
return self.get_all_inputs(data_only=True)
def get_all_outputs(self, data_only=False):
return []
def get_data_outputs(self):
return []
return self.get_all_outputs(data_only=True)
def get_post_job_actions(self, incoming):
return []
@@ -172,7 +179,7 @@ class WorkflowModule(object):
""" Replace connected inputs with placeholder/dummy values. """
pass
def get_config_form(self):
def get_config_form(self, step=None):
""" Serializes input parameters of a module into input dictionaries. """
return {
'title' : self.name,
@@ -361,7 +368,7 @@ class SubWorkflowModule(WorkflowModule):
return self.subworkflow.name
return self.name
def get_all_inputs(self, data_only=False):
def get_all_inputs(self, data_only=False, connectable_only=False):
""" Get configure time data input descriptions. """
# Filter subworkflow steps and get inputs
step_to_input_type = {
@@ -389,10 +396,7 @@ class SubWorkflowModule(WorkflowModule):
inputs.append(input)
return inputs
def get_data_inputs(self):
return self.get_all_inputs(data_only=True)
def get_data_outputs(self):
def get_all_outputs(self, data_only=False):
outputs = []
if hasattr(self.subworkflow, 'workflow_outputs'):
from galaxy.managers.workflows import WorkflowContentsManager
@@ -403,7 +407,7 @@ class SubWorkflowModule(WorkflowModule):
tooltip=False)
for order_index in sorted(subworkflow_dict['steps']):
step = subworkflow_dict['steps'][order_index]
data_outputs = subworkflow_dict['steps'][order_index]['data_outputs']
data_outputs = subworkflow_dict['steps'][order_index]['outputs']
for workflow_output in step['workflow_outputs']:
label = workflow_output['label']
if not label:
@@ -498,7 +502,7 @@ class InputModule(WorkflowModule):
state.inputs = dict(input=None)
return state
def get_data_inputs(self):
def get_all_inputs(self, data_only=False, connectable_only=False):
return []
def execute(self, trans, progress, invocation_step, use_cached_job=False):
@@ -538,7 +542,7 @@ class InputDataModule(InputModule):
type = "data_input"
name = "Input dataset"
def get_data_outputs(self):
def get_all_outputs(self, data_only=False):
return [dict(name='output', extensions=['input'])]
def get_filter_set(self, connections=None):
@@ -579,7 +583,7 @@ class InputDataCollectionModule(InputModule):
input_element = Element("param", name="input", label=self.label, type="data_collection", collection_type=collection_type)
return dict(input=DataCollectionToolParameter(None, input_element, self.trans))
def get_data_outputs(self):
def get_all_outputs(self, data_only=False):
return [
dict(
name='output',
@@ -604,14 +608,20 @@ class InputParameterModule(WorkflowModule):
optional = self.state.inputs.get("optional", self.default_optional)
input_parameter_type = SelectToolParameter(None, XML(
'''
<param name="parameter_type" label="Parameter type" type="select" value="%s">
<param name="parameter_type" label="Parameter type" type="select">
<option value="text">Text</option>
<option value="integer">Integer</option>
<option value="float">Float</option>
<option value="boolean">Boolean (True or False)</option>
<option value="color">Color</option>
</param>
''' % parameter_type))
'''))
for i, option in enumerate(input_parameter_type.static_options):
option = list(option)
if option[1] == parameter_type:
# item 0 is option description, item 1 is value, item 2 is "selected"
option[2] = True
input_parameter_type.static_options[i] = tuple(option)
return odict([("parameter_type", input_parameter_type),
("optional", BooleanToolParameter(None, Element("param", name="optional", label="Optional", type="boolean", value=optional)))])
@@ -635,8 +645,16 @@ class InputParameterModule(WorkflowModule):
state.inputs = dict(input=None)
return state
def get_data_inputs(self):
return []
def get_all_outputs(self, data_only=False):
if data_only:
return []
return [dict(
name='output',
label=self.label,
type=self.state.inputs.get('parameter_type', self.parameter_type),
parameter=True,
)]
def execute(self, trans, progress, invocation_step, use_cached_job=False):
step = invocation_step.workflow_step
@@ -651,7 +669,7 @@ class PauseModule(WorkflowModule):
type = "pause"
name = "Pause for dataset review"
def get_data_inputs(self):
def get_all_inputs(self, data_only=False, connectable_only=False):
input = dict(
name="input",
label="Dataset for Review",
@@ -659,9 +677,9 @@ class PauseModule(WorkflowModule):
extensions='input',
input_type="dataset",
)
return [input]
return [input] if not data_only else []
def get_data_outputs(self):
def get_all_outputs(self, data_only=False):
return [dict(name="output", label="Reviewed Dataset", extensions=['input'])]
def get_runtime_state(self):
@@ -803,14 +821,24 @@ class ToolModule(WorkflowModule):
def get_inputs(self):
return self.tool.inputs if self.tool else {}
def get_all_inputs(self, data_only=False):
def get_all_inputs(self, data_only=False, connectable_only=False):
if data_only and connectable_only:
raise Exception("Must specify at most one of data_only and connectable_only as True.")
inputs = []
if self.tool:
def callback(input, prefixed_name, prefixed_label, **kwargs):
def callback(input, prefixed_name, prefixed_label, value=None, **kwargs):
visible = not hasattr(input, 'hidden') or not input.hidden
input_type = input.type
is_data = isinstance(input, DataToolParameter) or isinstance(input, DataCollectionToolParameter)
skip = data_only and (not visible or not is_data)
is_connectable = is_runtime_value(value) and runtime_to_json(value)["__class__"] == "ConnectedValue"
if data_only:
skip = not visible or not is_data
elif connectable_only:
skip = not visible or not (is_data or is_connectable)
else:
skip = not visible
if not skip:
if isinstance(input, DataToolParameter):
inputs.append(dict(
@@ -835,16 +863,14 @@ class ToolModule(WorkflowModule):
label=prefixed_label,
multiple=False,
input_type="parameter",
type=input_type,
)
)
visit_input_values(self.tool.inputs, self.state.inputs, callback)
return inputs
def get_data_inputs(self):
return self.get_all_inputs(data_only=True)
def get_data_outputs(self):
def get_all_outputs(self, data_only=False):
data_outputs = []
if self.tool:
for name, tool_output in self.tool.outputs.items():
@@ -890,9 +916,9 @@ class ToolModule(WorkflowModule):
)
return data_outputs
def get_config_form(self):
def get_config_form(self, step=None):
if self.tool:
self.add_dummy_datasets()
self.add_dummy_datasets(connections=step and step.input_connections)
incoming = {}
params_to_incoming(incoming, self.tool.inputs, self.state.inputs, self.trans.app)
return self.tool.to_json(self.trans, incoming, workflow_building_mode=True)
@@ -911,22 +937,23 @@ class ToolModule(WorkflowModule):
# Any input needs to have value RuntimeValue or obtain the value from connected steps
def callback(input, prefixed_name, context, **kwargs):
if isinstance(input, DataToolParameter) or isinstance(input, DataCollectionToolParameter):
if connections is not None and steps is not None and self.trans.workflow_building_mode is workflow_building_modes.USE_HISTORY:
if prefixed_name in input_connections_by_name:
connection = input_connections_by_name[prefixed_name]
output_step = next(output_step for output_step in steps if connection.output_step_id == output_step.id)
if output_step.type.startswith('data'):
output_inputs = output_step.module.get_runtime_inputs(connections=connections)
output_value = output_inputs['input'].get_initial_value(self.trans, context)
if isinstance(input, DataToolParameter) and isinstance(output_value, self.trans.app.model.HistoryDatasetCollectionAssociation):
return output_value.to_hda_representative()
return output_value
return RuntimeValue()
else:
return input.get_initial_value(self.trans, context)
elif connections is None or prefixed_name in input_connections_by_name:
return RuntimeValue()
input_type = input.type
is_data = input_type in ['data', 'data_collection']
if is_data and connections is not None and steps is not None and self.trans.workflow_building_mode is workflow_building_modes.USE_HISTORY:
if prefixed_name in input_connections_by_name:
connection = input_connections_by_name[prefixed_name]
output_step = next(output_step for output_step in steps if connection.output_step_id == output_step.id)
if output_step.type.startswith('data'):
output_inputs = output_step.module.get_runtime_inputs(connections=connections)
output_value = output_inputs['input'].get_initial_value(self.trans, context)
if input_type == "data" and isinstance(output_value, self.trans.app.model.HistoryDatasetCollectionAssociation):
return output_value.to_hda_representative()
return output_value
return ConnectedValue()
else:
return input.get_initial_value(self.trans, context)
elif (is_data and connections is None) or prefixed_name in input_connections_by_name:
return ConnectedValue()
visit_input_values(self.tool.inputs, self.state.inputs, callback)
else:
raise ToolMissingException("Tool %s missing. Cannot add dummy datasets." % self.tool_id)
@@ -1300,6 +1327,11 @@ def load_module_sections(trans):
"title": "Input Dataset Collection",
"description": "Input dataset collection"
},
{
"name": "parameter_input",
"title": "Parameter Input",
"description": "Simple inputs used for workflow logic"
},
],
}
@@ -1312,12 +1344,7 @@ def load_module_sections(trans):
"name": "pause",
"title": "Pause Workflow for Dataset Review",
"description": "Pause for Review"
},
{
"name": "parameter_input",
"title": "Parameter Input",
"description": "Simple inputs used for workflow logic"
},
}
],
}
+7 -9
View File
@@ -352,13 +352,10 @@ class WorkflowProgress(object):
try:
replacement = step_outputs[output_name]
except KeyError:
if is_data:
# Must resolve.
template = "Workflow evaluation problem - failed to find output_name %s in step_outputs %s"
message = template % (output_name, step_outputs)
raise Exception(message)
else:
replacement = modules.NO_REPLACEMENT
# Must resolve.
template = "Workflow evaluation problem - failed to find output_name %s in step_outputs %s"
message = template % (output_name, step_outputs)
raise Exception(message)
if isinstance(replacement, model.HistoryDatasetCollectionAssociation):
if not replacement.collection.populated:
if not replacement.collection.waiting_for_elements:
@@ -390,11 +387,12 @@ class WorkflowProgress(object):
if self.inputs_by_step_id:
step_id = step.id
if step_id not in self.inputs_by_step_id:
if step_id not in self.inputs_by_step_id and 'output' not in outputs:
template = "Step with id %s not found in inputs_step_id (%s)"
message = template % (step_id, self.inputs_by_step_id)
raise ValueError(message)
outputs['output'] = self.inputs_by_step_id[step_id]
elif step_id in self.inputs_by_step_id:
outputs['output'] = self.inputs_by_step_id[step_id]
self.set_step_outputs(invocation_step, outputs)
+11
View File
@@ -265,6 +265,17 @@ def build_workflow_run_configs(trans, workflow, payload):
# inputs with referential integrity if parameters are already normalized (coming from tool form).
normalized_inputs = {}
if param_map:
# disentangle raw parameter dictionaries into formal request structures if we can
# to setup proper WorkflowRequestToInputDatasetAssociation, WorkflowRequestToInputDatasetCollectionAssociation
# and WorkflowRequestInputStepParameter objects.
for step in workflow.steps:
normalized_key = step.id
if step.type == "parameter_input":
if normalized_key in param_map:
value = param_map.pop(normalized_key)
normalized_inputs[normalized_key] = value["input"]
steps_by_id = workflow.steps_by_id
# Set workflow inputs.
for key, input_dict in normalized_inputs.items():
+4 -4
View File
@@ -468,8 +468,8 @@ class WorkflowsApiTestCase(BaseWorkflowsApiTestCase):
'name',
'tool_state',
'tooltip',
'data_inputs',
'data_outputs',
'inputs',
'outputs',
'config_form',
'annotation',
'post_job_actions',
@@ -494,7 +494,7 @@ steps:
downloaded_workflow = self._download_workflow(workflow_id, style="editor")
steps = downloaded_workflow['steps']
assert len(steps) == 2
assert steps['1']['data_outputs'][0]['collection_type'] == 'list:paired'
assert steps['1']['outputs'][0]['collection_type'] == 'list:paired'
@skip_without_tool('collection_type_source')
def test_export_editor_subworkflow_collection_type_source(self):
@@ -525,7 +525,7 @@ steps:
steps = downloaded_workflow['steps']
assert len(steps) == 2
assert steps['1']['type'] == 'subworkflow'
assert steps['1']['data_outputs'][0]['collection_type'] == 'list:paired'
assert steps['1']['outputs'][0]['collection_type'] == 'list:paired'
def test_import_missing_tool(self):
workflow = self.workflow_populator.load_workflow_from_resource(name="test_workflow_missing_tool")
+5 -1
View File
@@ -236,7 +236,8 @@ workflows:
workflow_run:
selectors:
input_div: "[step-label='${label}'] .select2-container"
input_div: "[step-label='${label}']"
input_data_div: "[step-label='${label}'] .select2-container"
# TODO: put step labels in the DOM ideally
subworkflow_step_icon: ".portlet-title-icon.fa-sitemap + span"
@@ -264,6 +265,9 @@ workflow_editor:
tool_menu_section_link: '#title___workflow__${section_name}__ a span'
tool_menu_item_link: 'a#tool-menu-${section_name}-${item_name}'
connect_icon: 'div.ui-form-element[tour_id="${name}"] .ui-form-connected-icon'
collapse_icon: 'div.ui-form-element[tour_id="${name}"] .ui-form-collapsible-icon'
label_input: "[tour_id='__label'] input"
annotation_input: "[tour_id='__annotation'] textarea"
+90 -11
View File
@@ -54,10 +54,7 @@ class WorkflowEditorTestCase(SeleniumTestCase):
editor = self.components.workflow_editor
name = self.workflow_create_new()
editor.canvas_body.wait_for_visible()
editor.tool_menu.wait_for_visible()
editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
editor.tool_menu_item_link(section_name="inputs", item_name="data_input").wait_for_and_click()
self.workflow_editor_add_input(item_name="data_input")
self.screenshot("workflow_editor_data_input_new")
editor.label_input.wait_for_and_send_keys("input1")
editor.annotation_input.wait_for_and_send_keys("my cool annotation")
@@ -85,16 +82,13 @@ class WorkflowEditorTestCase(SeleniumTestCase):
editor = self.components.workflow_editor
name = self.workflow_create_new()
editor.canvas_body.wait_for_visible()
editor.tool_menu.wait_for_visible()
editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
editor.tool_menu_item_link(section_name="inputs", item_name="data_collection_input").wait_for_and_click()
self.screenshot("workflow_editor_data_input_collection_new")
self.workflow_editor_add_input(item_name="data_collection_input")
self.screenshot("workflow_editor_data_collection_input_new")
editor.label_input.wait_for_and_send_keys("input1")
editor.annotation_input.wait_for_and_send_keys("my cool annotation")
editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation.
self.sleep_for(self.wait_types.UX_RENDER)
self.screenshot("workflow_editor_data_input_collection_filled_in")
self.screenshot("workflow_editor_data_collection_input_filled_in")
self.workflow_editor_save_and_close()
self.workflow_index_open_with_name(name)
data_input_node = editor.node._(label="input1")
@@ -107,7 +101,82 @@ class WorkflowEditorTestCase(SeleniumTestCase):
data_input_node.destroy.wait_for_and_click()
data_input_node.wait_for_absent()
self.screenshot("workflow_editor_data_input_deleted")
self.screenshot("workflow_editor_data_collection_input_deleted")
@selenium_test
def test_integer_input(self):
editor = self.components.workflow_editor
name = self.workflow_create_new()
self.workflow_editor_add_input(item_name="parameter_input")
self.screenshot("workflow_editor_parameter_input_new")
editor.label_input.wait_for_and_send_keys("input1")
editor.annotation_input.wait_for_and_send_keys("my cool annotation")
editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation.
self.sleep_for(self.wait_types.UX_RENDER)
self.screenshot("workflow_editor_parameter_input_filled_in")
self.workflow_editor_save_and_close()
self.workflow_index_open_with_name(name)
data_input_node = editor.node._(label="input1")
data_input_node.title.wait_for_and_click()
label = editor.label_input.wait_for_value()
assert label == "input1", label
# should work but Galaxy is broken.
# assert editor.annotation_input.wait_for_value() == "my cool annotation"
data_input_node.destroy.wait_for_and_click()
data_input_node.wait_for_absent()
self.screenshot("workflow_editor_parameter_input_deleted")
@selenium_test
def test_non_data_connections(self):
self.open_in_workflow_editor("""
class: GalaxyWorkflow
inputs:
input_int: integer
steps:
simple_constructs:
tool_id: simple_constructs
label: tool_exec
in:
inttest: input_int
""")
self.screenshot("workflow_editor_parameter_connection_simple")
self.assert_connected("input_int#output", "simple_constructs#inttest")
editor = self.components.workflow_editor
tool_node = editor.node._(label="simple_constructs")
tool_input = tool_node.input_terminal(name="inttest")
tool_input.wait_for_and_click()
editor.connector_destroy_callout.wait_for_and_click()
self.assert_not_connected("input_int#output", "simple_constructs#inttest")
self.screenshot("workflow_editor_parameter_connection_destroyed")
# When connected, cannot turn it into a RuntimeValue..
collapse_input = editor.collapse_icon(name="inttest")
collapse_input.wait_for_absent_or_hidden()
# If it is disconnected, then can specify as RuntimeValue
connect_icon = editor.connect_icon(name="inttest")
connect_icon.wait_for_visible()
connect_icon.wait_for_and_click()
collapse_input.wait_for_visible()
# Also the connector should disappear
tool_input.wait_for_absent_or_hidden()
# Now make it connected again and watch the requestss
connect_icon.wait_for_and_click()
tool_input.wait_for_visible()
collapse_input.wait_for_absent_or_hidden()
self.workflow_editor_connect("input_int#output", "simple_constructs#inttest", screenshot_partial="workflow_editor_parameter_connection_dragging")
self.assert_connected("input_int#output", "simple_constructs#inttest")
@selenium_test
def test_existing_connections(self):
@@ -329,6 +398,16 @@ steps:
return source_id, sink_id
def workflow_editor_add_input(self, item_name="data_input"):
editor = self.components.workflow_editor
# Make sure we're on the the workflow editor and not clicking the main tool panel.
editor.canvas_body.wait_for_visible()
editor.tool_menu.wait_for_visible()
editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
editor.tool_menu_item_link(section_name="inputs", item_name=item_name).wait_for_and_click()
def workflow_editor_destroy_connection(self, sink):
editor = self.components.workflow_editor
+34 -1
View File
@@ -96,6 +96,39 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions):
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_step_parameter_inputs(self):
self.perform_upload(self.get_filename("1.txt"))
self.wait_for_history()
self.open_in_workflow_run("""
class: GalaxyWorkflow
inputs:
input_int: integer
input_data: data
steps:
simple_constructs:
tool_id: simple_constructs
label: tool_exec
in:
inttest: input_int
files_0|file: input_data
""")
workflow_run = self.components.workflow_run
input_div_element = workflow_run.input_div(label="input_int").wait_for_visible()
input_element = input_div_element.find_element_by_css_selector("input")
input_element.clear()
input_element.send_keys("12345")
self.screenshot("workflow_run_step_parameter_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()
content = self.dataset_populator.get_history_dataset_content(history_id, hid=output_hid)
assert "12345" in content, content
assert "chr6_hla_hap2" in content
@selenium_test
@managed_history
def test_replacement_parameters_on_subworkflows(self):
@@ -160,7 +193,7 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions):
def workflow_run_specify_inputs(self, inputs):
workflow_run = self.components.workflow_run
for label, value in inputs.items():
input_div_element = workflow_run.input_div(label=label).wait_for_visible()
input_div_element = workflow_run.input_data_div(label=label).wait_for_visible()
self.select2_set_value(input_div_element, "%d: " % value["hid"])
def workflow_run_with_name(self, name):