Merge branch 'dev' of github.com:anuprulez/galaxy into recommend_tools_run

This commit is contained in:
Anup Kumar
2020-03-12 15:01:31 +01:00
42 changed files with 883 additions and 478 deletions
@@ -5,16 +5,16 @@
{{ message }}
</b-alert>
<p>
You are logged in as <strong>{{ email }}</strong
You are logged in as <strong id="user-preferences-current-email">{{ email }}</strong
>.
</p>
<b-row class="ml-3 mb-1" v-for="(link, index) in activeLinks" :key="index">
<i :class="['pref-icon pt-1 fa fa-lg', link.icon]" />
<div class="pref-content pr-1">
<a v-if="link.onclick" @click="link.onclick" href="javascript:void(0)"
<a :id="link.id" v-if="link.onclick" @click="link.onclick" href="javascript:void(0)"
><b>{{ link.title }}</b></a
>
<a v-else :href="`${baseUrl}/${link.action}`"
<a :id="link.id" v-else :href="`${baseUrl}/${link.action}`"
><b>{{ link.title }}</b></a
>
<div class="form-text text-muted">
@@ -7,6 +7,7 @@ export const getUserPreferencesModel = () => {
return {
information: {
title: _l("Manage Information"),
id: "edit-preferences-information",
description: "Edit your email, addresses and custom parameters or change your public name.",
url: `api/users/${Galaxy.user.id}/information/inputs`,
icon: "fa-user",
@@ -15,6 +16,7 @@ export const getUserPreferencesModel = () => {
},
password: {
title: _l("Change Password"),
id: "edit-preferences-password",
description: _l("Allows you to change your login credentials."),
icon: "fa-unlock-alt",
url: `api/users/${Galaxy.user.id}/password/inputs`,
@@ -24,6 +26,7 @@ export const getUserPreferencesModel = () => {
},
communication: {
title: _l("Change Communication Settings"),
id: "edit-preferences-communication",
description: _l("Enable or disable the communication feature to chat with other users."),
url: `api/users/${Galaxy.user.id}/communication/inputs`,
icon: "fa-comments-o",
@@ -32,6 +35,7 @@ export const getUserPreferencesModel = () => {
},
permissions: {
title: _l("Set Dataset Permissions for new Histories"),
id: "edit-preferences-permissions",
description:
"Grant others default access to newly created histories. Changes made here will only affect histories created after these settings have been stored.",
url: `api/users/${Galaxy.user.id}/permissions/inputs`,
@@ -41,11 +45,13 @@ export const getUserPreferencesModel = () => {
},
make_data_private: {
title: _l("Make all Data Private"),
id: "edit-preferences-make-data-private",
description: _l("Click here to make all data private."),
icon: "fa-lock"
},
api_key: {
title: _l("Manage API Key"),
id: "edit-preferences-api-key",
description: _l("Access your current API key or create a new one."),
url: `api/users/${Galaxy.user.id}/api_key/inputs`,
icon: "fa-key",
@@ -53,6 +59,7 @@ export const getUserPreferencesModel = () => {
submit_icon: "fa-check"
},
cloud_auth: {
id: "edit-preferences-cloud-auth",
title: _l("Manage Cloud Authorization"),
description: _l("Add or modify the configuration that grants Galaxy to access your cloud-based resources."),
icon: "fa-cloud",
@@ -61,6 +68,7 @@ export const getUserPreferencesModel = () => {
},
toolbox_filters: {
title: _l("Manage Toolbox Filters"),
id: "edit-preferences-toolbox-filters",
description: _l("Customize your Toolbox by displaying or omitting sets of Tools."),
url: `api/users/${Galaxy.user.id}/toolbox_filters/inputs`,
icon: "fa-filter",
@@ -75,6 +83,7 @@ export const getUserPreferencesModel = () => {
},
logout: {
title: _l("Sign Out"),
id: "edit-preferences-custom-builds",
description: _l("Click here to sign out of all sessions."),
icon: "fa-sign-out",
shouldRender: !!Galaxy.session_csrf_token
@@ -1,9 +1,10 @@
<template>
<div>
<div class="toolFormTitle unselectable clearfix">
<i :class="iconClass" />
<span class="node-title">{{ title }}</span>
<span class="sr-only">&nbsp;Node</span>
<div class="node-header unselectable clearfix">
<div class="header float-left">
<i :class="iconClass" />
<span class="node-title">{{ title }}</span>
</div>
<div class="buttons float-right">
<a
v-if="canClone"
@@ -22,7 +23,7 @@
/>
</div>
</div>
<div class="toolFormBody">
<div class="node-body">
<div>
<loading-span message="Loading details" />
</div>
@@ -177,6 +177,7 @@ export default Backbone.View.extend({
this.$title_text.show().text(this.model.get("label"));
this.$collapsible.hide();
}
this.$field.attr("data-label", this.model.get("label"));
},
_template: function() {
@@ -212,10 +212,14 @@ var HistoryViewColumn = Backbone.View.extend(baseMVC.LoggableMixin).extend({
`<div class="text-right col-4">
<% if( !data.history.purged ){ %>
<div class="panel-menu btn-group">
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown">
<button
history-dropdown-btn="<%= data.history.id %>"
type="button"
class="btn btn-secondary dropdown-toggle"
data-toggle="dropdown">
<span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<div history-dropdown-menu="<%= data.history.id %>" class="dropdown-menu" role="menu">
<% if( !data.history.deleted ){ %>
<a class="dropdown-item copy-history" href="javascript:void(0);">
${_l("Copy")}
@@ -1,19 +1,35 @@
import $ from "jquery";
import * as d3 from "d3";
import { Toast } from "ui/toast";
function Connector(manager, handle1, handle2) {
this.manager = manager;
this.canvas = null;
this.dragging = false;
this.inner_color = "#FFFFFF";
this.outer_color = "#25537b";
if (handle1 && handle2) {
this.connect(handle1, handle2);
// Rendering parameters
const zIndex = 1000;
const cpFactor = 10;
const canvasExtra = 100;
const handleMarginX = 8;
const handleMarginY = 5;
const ribbonMargin = 4;
const ribbonInnerSingle = 4;
const ribbonOuterSingle = 6;
const ribbonInnerMultiple = 1;
const ribbonOuterMultiple = 3;
class Connector {
constructor(manager = {}, handle1 = null, handle2 = null) {
this.manager = manager;
this.dragging = false;
this.innerClass = "ribbon-inner";
this.canvas = document.createElement("div");
this.canvas.style.position = "absolute";
const container = document.getElementById("canvas-container");
container.appendChild(this.canvas);
this.svg = d3.select(this.canvas).append("svg");
this.svg.attr("class", "ribbon");
if (handle1 && handle2) {
this.connect(handle1, handle2);
}
}
}
$.extend(Connector.prototype, {
connect: function(t1, t2) {
connect(t1, t2) {
this.handle1 = t1;
if (this.handle1) {
this.handle1.connect(this);
@@ -22,38 +38,42 @@ $.extend(Connector.prototype, {
if (this.handle2) {
this.handle2.connect(this);
}
},
destroy: function() {
}
destroy() {
if (this.handle1) {
this.handle1.disconnect(this);
}
if (this.handle2) {
this.handle2.disconnect(this);
}
$(this.canvas).remove();
},
destroyIfInvalid: function(warn) {
this.canvas.remove();
}
destroyIfInvalid(warn) {
if (this.handle1 && this.handle2 && !this.handle2.attachable(this.handle1).canAccept) {
if (warn) {
Toast.warning("Destroying a connection because collection type has changed.");
}
this.destroy();
}
},
redraw: function() {
}
dropStart(canAccept) {
if (canAccept) {
this.innerClass = "ribbon-inner-valid";
} else {
this.innerClass = "ribbon-inner-invalid";
}
}
dropEnd() {
this.innerClass = "ribbon-inner";
}
redraw() {
// Identify handles
const handle1 = this.handle1;
const handle2 = this.handle2;
const startRibbon = handle1 && handle1.isMappedOver();
const endRibbon = handle2 && handle2.isMappedOver();
const canvasClass = `${startRibbon ? "start-ribbon" : ""} ${endRibbon ? "end-ribbon" : ""}`;
var canvas_container = $("#canvas-container");
var canvasZoom = this.manager.canvasZoom;
if (!this.canvas) {
this.canvas = document.createElement("canvas");
canvas_container.append($(this.canvas));
if (this.dragging) {
this.canvas.style.zIndex = "300";
}
const canvas_container = $("#canvas-container");
const canvasZoom = this.manager.canvasZoom;
if (this.dragging) {
this.canvas.style.zIndex = zIndex;
}
this.canvas.setAttribute(
"handle1-id",
@@ -63,66 +83,75 @@ $.extend(Connector.prototype, {
"handle2-id",
handle2 && handle2.element.getAttribute ? handle2.element.getAttribute("id") : ""
);
var relativeLeft = e => ($(e).offset().left - canvas_container.offset().left) / canvasZoom;
var relativeTop = e => ($(e).offset().top - canvas_container.offset().top) / canvasZoom;
const relativeLeft = e => ($(e).offset().left - canvas_container.offset().left) / canvasZoom;
const relativeTop = e => ($(e).offset().top - canvas_container.offset().top) / canvasZoom;
if (!handle1 || !handle2) {
return;
}
// Find the position of each handle
var start_x = relativeLeft(handle1.element) + 5;
var start_y = relativeTop(handle1.element) + 5;
var end_x = relativeLeft(handle2.element) + 5;
var end_y = relativeTop(handle2.element) + 5;
let start_x = relativeLeft(handle1.element) + handleMarginX;
let start_y = relativeTop(handle1.element) + handleMarginY;
let end_x = relativeLeft(handle2.element) + handleMarginX;
let end_y = relativeTop(handle2.element) + handleMarginY;
// Calculate canvas area
var canvas_extra = 100;
var canvas_min_x = Math.min(start_x, end_x);
var canvas_max_x = Math.max(start_x, end_x);
var canvas_min_y = Math.min(start_y, end_y);
var canvas_max_y = Math.max(start_y, end_y);
var cp_shift = Math.min(Math.max(Math.abs(canvas_max_y - canvas_min_y) / 2, 100), 300);
var canvas_left = canvas_min_x - canvas_extra;
var canvas_top = canvas_min_y - canvas_extra;
var canvas_width = canvas_max_x - canvas_min_x + 2 * canvas_extra;
var canvas_height = canvas_max_y - canvas_min_y + 2 * canvas_extra;
const canvas_min_x = Math.min(start_x, end_x);
const canvas_max_x = Math.max(start_x, end_x);
const canvas_min_y = Math.min(start_y, end_y);
const canvas_max_y = Math.max(start_y, end_y);
const canvas_left = canvas_min_x - canvasExtra;
const canvas_top = canvas_min_y - canvasExtra;
const canvas_width = canvas_max_x - canvas_min_x + 2 * canvasExtra;
const canvas_height = canvas_max_y - canvas_min_y + 2 * canvasExtra;
// Place the canvas
this.canvas.style.left = `${canvas_left}px`;
this.canvas.style.top = `${canvas_top}px`;
this.canvas.setAttribute("width", canvas_width);
this.canvas.setAttribute("height", canvas_height);
this.canvas.setAttribute("class", canvasClass);
// Resize the svg
this.svg.style("width", `${canvas_width}px`);
this.svg.style("height", `${canvas_height}px`);
// Adjust points to be relative to the canvas
start_x -= canvas_left;
start_y -= canvas_top;
end_x -= canvas_left;
end_y -= canvas_top;
// Draw the line
// Determine line shift
const cp_shift = Math.min(Math.max(Math.abs(canvas_max_y - canvas_min_y) / 2, cpFactor), 3 * cpFactor);
var start_offsets = null;
var end_offsets = null;
var num_offsets = 1;
// Draw ribbons
this.drawRibbon(handle1, handle2, cp_shift, start_x, start_y, end_x, end_y);
}
drawRibbon(handle1, handle2, cp_shift, start_x, start_y, end_x, end_y) {
// Check ribbon type
const startRibbon = handle1 && handle1.isMappedOver();
const endRibbon = handle2 && handle2.isMappedOver();
// Draw the line
let start_offsets = [0];
let end_offsets = [0];
const offsets = [-2 * ribbonMargin, -ribbonMargin, 0, ribbonMargin, 2 * ribbonMargin];
let num_offsets = 1;
if (startRibbon) {
start_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
start_offsets = [0];
start_offsets = offsets;
num_offsets = offsets.length;
}
if (endRibbon) {
end_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
end_offsets = [0];
end_offsets = offsets;
num_offsets = offsets.length;
}
var connector = this;
for (var i = 0; i < num_offsets; i++) {
var inner_width = 5;
var outer_width = 7;
this.svg.selectAll("*").remove();
for (let i = 0; i < num_offsets; i++) {
let inner_width = ribbonInnerSingle;
let outer_width = ribbonOuterSingle;
if (start_offsets.length > 1 || end_offsets.length > 1) {
// We have a multi-run, using many lines, make them small.
inner_width = 1;
outer_width = 3;
inner_width = ribbonInnerMultiple;
outer_width = ribbonOuterMultiple;
}
connector.draw_outlined_curve(
this.drawCurve(
start_x,
start_y,
end_x,
@@ -134,49 +163,37 @@ $.extend(Connector.prototype, {
end_offsets[i % end_offsets.length]
);
}
},
draw_outlined_curve: function(
start_x,
start_y,
end_x,
end_y,
cp_shift,
inner_width,
outer_width,
offset_start,
offset_end
) {
var c = this.canvas.getContext("2d");
}
drawCurve(start_x, start_y, end_x, end_y, cp_shift, inner_width, outer_width, offset_start, offset_end) {
offset_start = offset_start || 0;
offset_end = offset_end || 0;
c.lineCap = "round";
c.strokeStyle = this.outer_color;
c.lineWidth = outer_width;
c.beginPath();
c.moveTo(start_x, start_y + offset_start);
c.bezierCurveTo(
start_x + cp_shift,
start_y + offset_start,
end_x - cp_shift,
end_y + offset_end,
end_x,
end_y + offset_end
);
c.stroke();
// Inner line
c.strokeStyle = this.inner_color;
c.lineWidth = inner_width;
c.beginPath();
c.moveTo(start_x, start_y + offset_start);
c.bezierCurveTo(
start_x + cp_shift,
start_y + offset_start,
end_x - cp_shift,
end_y + offset_end,
end_x,
end_y + offset_end
);
c.stroke();
const lineData = [
{ x: start_x, y: start_y + offset_start },
{ x: start_x + cp_shift, y: start_y + offset_start },
{ x: end_x - cp_shift, y: end_y + offset_end },
{ x: end_x, y: end_y + offset_end }
];
const lineFunction = d3.svg
.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.interpolate("basis");
this.svg
.append("path")
.attr("class", "ribbon-outer")
.attr("d", lineFunction(lineData))
.attr("stroke-width", outer_width)
.attr("fill", "none");
this.svg
.append("path")
.attr("class", this.innerClass)
.attr("d", lineFunction(lineData))
.attr("stroke-width", inner_width)
.attr("fill", "none");
}
});
}
export default Connector;
@@ -134,7 +134,7 @@ class Workflow extends EventEmitter {
// Create node wrapper
const container = document.createElement("div");
container.className = "toolForm toolFormInCanvas";
container.className = "workflow-node";
document.getElementById("canvas-container").appendChild(container);
var $f = $(container);
@@ -380,15 +380,15 @@ class Workflow extends EventEmitter {
wf.id_counter = max_id + 1;
// Second pass, connections
$.each(data.steps, (id, step) => {
var node = wf.nodes[parseInt(id) + offset];
const node = wf.nodes[parseInt(id) + offset];
$.each(step.input_connections, (k, v) => {
if (v) {
if (!$.isArray(v)) {
v = [v];
}
$.each(v, (l, x) => {
var other_node = wf.nodes[parseInt(x.id) + offset];
var c = new Connector(this.canvas_manager);
const other_node = wf.nodes[parseInt(x.id) + offset];
const c = new Connector(this.canvas_manager);
c.connect(other_node.output_terminals[x.output_name], node.input_terminals[k]);
c.redraw();
});
@@ -10,7 +10,7 @@ export class Node {
this.element = attr.element;
this.input_terminals = {};
this.output_terminals = {};
this.errors = {};
this.errors = null;
this.workflow_outputs = [];
}
getWorkflowOutput(outputName) {
@@ -191,7 +191,7 @@ export class Node {
$(this.element).remove();
}
make_active() {
$(this.element).addClass("toolForm-active");
$(this.element).addClass("node-active");
}
make_inactive() {
// Keep inactive nodes stacked from most to least recently active
@@ -202,7 +202,7 @@ export class Node {
p.appendChild(element);
})(element.parentNode);
// Remove active class
$(element).removeClass("toolForm-active");
$(element).removeClass("node-active");
}
set_tool_version() {
if (this.type === "tool" && this.config_form) {
@@ -309,7 +309,7 @@ export class Node {
var pja_in = data.post_job_actions;
this.post_job_actions = pja_in ? pja_in : {};
}
node.nodeView.renderToolErrors();
node.nodeView.renderErrors();
// Update input rows
var old_body = nodeView.$el.find("div.inputs");
var new_body = nodeView.newInputsDiv();
@@ -340,14 +340,6 @@ export class Node {
this.markChanged();
this.redraw();
}
error(text) {
var b = $(this.element).find(".toolFormBody");
b.find("div").remove();
var tmp = `<div style='color: red; text-style: italic;'>${text}</div>`;
this.config_form = tmp;
b.html(tmp);
this.app.node_changed(this);
}
markChanged() {
this.app.node_changed(this);
}
@@ -9,26 +9,31 @@ export class NodeView {
this.$el = options.$el;
this.node = options.node;
this.output_width = Math.max(150, this.$el.width());
this.tool_body = this.$el.find(".toolFormBody");
this.tool_body.find("div").remove();
this.newInputsDiv().appendTo(this.tool_body);
this.node_body = this.$el.find(".node-body");
this.node_body.find("div").remove();
this.newInputsDiv().appendTo(this.node_body);
this.terminalViews = {};
this.outputViews = {};
}
render() {
this.renderToolLabel();
this.renderToolErrors();
this.renderLabel();
this.renderErrors();
this.$el.css("width", Math.min(250, Math.max(this.$el.width(), this.output_width)));
}
renderToolLabel() {
renderLabel() {
this.$el.find(".node-title").text(this.node.label || this.node.name);
this.$el.attr("node-label", this.node.label);
}
renderToolErrors() {
this.node.errors ? this.$el.addClass("tool-node-error") : this.$el.removeClass("tool-node-error");
renderErrors() {
if (this.node.errors) {
this.$el.addClass("node-error");
this.node_body.text(this.node.errors);
} else {
this.$el.removeClass("node-error");
}
}
newInputsDiv() {
@@ -40,7 +45,7 @@ export class NodeView {
}
addRule() {
this.tool_body.append($("<div/>").addClass("rule"));
this.node_body.append($("<div/>").addClass("rule"));
}
addDataInput(input, body) {
@@ -111,7 +116,7 @@ export class NodeView {
const terminalView = this.terminalViewForOutput(output);
const outputView = this.outputViewforOutput(output, terminalView);
this.outputViews[output.name] = outputView;
this.tool_body.append(outputView.$el.append(terminalView.terminalElements()));
this.node_body.append(outputView.$el.append(terminalView.terminalElements()));
}
redrawWorkflowOutputs() {
@@ -91,10 +91,10 @@ class BaseInputTerminalView extends TerminalView {
onDropStart(e, d = {}) {
if (d.proxy.terminal) {
if (this.$el.hasClass("can-accept")) {
d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
d.proxy.terminal.connectors[0].dropStart(true);
d.proxy.dropTooltip = "";
} else {
d.proxy.terminal.connectors[0].inner_color = "#fe7f02";
d.proxy.terminal.connectors[0].dropStart(false);
if (this.reason) {
d.proxy.dropTooltip = this.reason;
$(d.proxy).tooltip("show");
@@ -107,14 +107,15 @@ class BaseInputTerminalView extends TerminalView {
onDropEnd(e, d = {}) {
d.proxy.dropTooltip = "";
if (d.proxy.terminal) {
d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
d.proxy.terminal.connectors[0].dropEnd();
}
}
onDrop(e, d = {}) {
d.proxy.dropTooltip = "";
if (this.$el.hasClass("can-accept")) {
const terminal = this.el.terminal;
new Connector(this.app.canvas_manager, d.drag.terminal, terminal).redraw();
const c = new Connector(this.app.canvas_manager, d.drag.terminal, terminal);
c.redraw();
}
}
onHover() {
@@ -357,7 +358,7 @@ export class BaseOutputTerminalView extends TerminalView {
}
});
h.terminal = new Terminals.OutputTerminal({ element: h });
var c = new Connector(this.app.canvas_manager);
const c = new Connector(this.app.canvas_manager);
c.dragging = true;
c.connect(this.el.terminal, h.terminal);
return h;
@@ -242,9 +242,7 @@ QUnit.test("default attributes", function(assert) {
const output = { connect: sinon.spy() };
const connector = new Connector({}, input, output);
assert.equal(connector.dragging, false);
assert.equal(connector.canvas, null);
assert.equal(connector.inner_color, "#FFFFFF");
assert.equal(connector.outer_color, "#25537b");
assert.equal(connector.svg.attr("class"), "ribbon");
});
QUnit.test("destroy", function(assert) {
@@ -271,13 +269,12 @@ QUnit.test("initial redraw", function(assert) {
return false;
}
};
const n = $("#canvas-container").find("svg").length;
const connector = new Connector({}, input, output);
const n = $("#canvas-container").find("canvas").length;
connector.redraw();
// Ensure canvas gets set
assert.ok(connector.canvas);
// Ensure it got added to canvas container
assert.equal(n + 1, $("#canvas-container").find("canvas").length);
assert.equal(n + 1, $("#canvas-container").find("svg").length);
});
QUnit.module("Input collection terminal model test", {
@@ -366,9 +363,9 @@ QUnit.module("Node unit test", {
});
QUnit.test("make active", function(assert) {
assert.ok(!this.element.hasClass("toolForm-active"));
assert.ok(!this.element.hasClass("node-active"));
this.node.make_active();
assert.ok(this.element.hasClass("toolForm-active"));
assert.ok(this.element.hasClass("node-active"));
});
QUnit.test("destroy", function(assert) {
@@ -380,11 +377,11 @@ QUnit.test("destroy", function(assert) {
});
QUnit.test("error", function(assert) {
// Test body of toolFormBody div updated and workflow notified of change.
// Test body of div updated and workflow notified of change.
var test = this;
this.expect_workflow_node_changed(assert, function() {
test.node.error("TOOL ERROR");
assert.equal($(test.$(".toolFormBody").children()[0]).html(), "TOOL ERROR");
test.node.init_field_data({ errors: "NODE ERROR", inputs: [], outputs: [] });
assert.equal(test.$(".node-body").html(), "NODE ERROR");
});
});
@@ -428,10 +425,10 @@ QUnit.test("init_field_data data", function(assert) {
assert.equal(this.$(".output-terminal").length, 1);
assert.equal(this.$(".input-terminal").length, 1);
assert.equal(this.$(".rule").length, 1);
assert.equal(this.$(".toolFormBody").children().length, 3);
assert.equal(this.$(".node-body").children().length, 3);
assert.equal(this.$(".node-title").text(), "newnode");
assert.ok(
this.$(".toolFormTitle")
this.$(".node-header")
.find("i")
.hasClass("fa-wrench")
);
@@ -607,10 +604,10 @@ QUnit.module("Node view ", {
QUnit.test("tool error styling", function(assert) {
this.set_for_node({ errors: false });
this.view.render();
assert.ok(!this.view.$el.hasClass("tool-node-error"));
assert.ok(!this.view.$el.hasClass("node-error"));
this.set_for_node({ errors: true });
this.view.render();
assert.ok(this.view.$el.hasClass("tool-node-error"));
assert.ok(this.view.$el.hasClass("node-error"));
});
QUnit.test("rendering correct width", function(assert) {
+30 -12
View File
@@ -7,40 +7,44 @@
display: flex;
flex-direction: column;
flex-grow: 1;
.toolForm.toolFormInCanvas {
.workflow-node {
@extend .card;
@extend .position-absolute;
z-index: 100;
width: $worfklow-node-width;
position: absolute;
border-color: $brand-primary;
&.toolForm-active {
border: solid $brand-primary 1px;
&.node-active {
z-index: 1001;
border: solid $white 1px;
box-shadow: 0 0 0 2px $brand-primary;
}
.toolFormTitle {
.node-header {
@extend .card-header;
@extend .py-1;
@extend .px-2;
cursor: move;
background: $brand-secondary;
.header {
width: $worfklow-node-width - 2rem;
}
}
.toolFormBody {
.node-body {
@extend .card-body;
@extend .p-0;
@extend .mx-2;
.rule {
height: 0;
border: none;
border-bottom: dotted black 1px;
border-bottom: dotted $brand-primary 1px;
margin: 0 5px;
}
}
}
.toolForm.toolFormInCanvas.tool-node-error {
border-color: $state-danger-border;
.toolFormTitle {
background: $state-danger-bg;
border-color: $state-danger-border;
.workflow-node.node-error {
border-color: $brand-danger;
.node-header {
background: $brand-danger;
border-color: $brand-danger;
}
}
.workflow-canvas {
@@ -55,6 +59,20 @@
position: relative;
overflow: hidden;
}
.ribbon {
.ribbon-outer {
stroke: $brand-primary;
}
.ribbon-inner {
stroke: $white;
}
.ribbon-inner-valid {
stroke: $brand-success;
}
.ribbon-inner-invalid {
stroke: $brand-warning;
}
}
.base-terminal {
@extend .fa;
@extend .fa-circle;
@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
<%inherit file="visualization_base.mako"/>
<%def name="stylesheets()">
${h.css('jquery-ui/smoothness/jquery-ui')}
${h.css('base')}
${h.dist_css('base')}
<%css_path = script_attributes.get("css") %>
%if css_path is not None:
<link rel="stylesheet" href="${static_url}${css_path}">
%endif
</%def>
<%def name="javascripts()">
${h.js('libs/jquery/jquery',
'libs/jquery/jquery-ui',
'bundled/libs.chunk',
'bundled/base.chunk',
'bundled/generic.bundled')}
${h.dist_js(
'libs.chunk',
'base.chunk',
'generic.bundled')}
<%src_path = script_attributes.get("src") %>
<script type="text/javascript" src="${static_url}${src_path}"></script>
<script type="text/javascript">
@@ -44,10 +44,6 @@ ${h.css('base')}
## Default javascripts
<%def name="javascripts()">
${h.js(
"libs/jquery/jquery",
"libs/jquery/jquery.migrate"
)}
## connect to Galaxy here
<script type="text/javascript">
@@ -70,8 +66,4 @@ ${h.js(
## Default javascripts
<%def name="late_javascripts()">
${h.js(
"libs/jquery/jquery",
"libs/jquery/jquery.migrate"
)}
</%def>
+18 -1
View File
@@ -1221,6 +1221,21 @@
:Type: str
~~~~~~~~~~~~~~~~~~
``whitelist_file``
~~~~~~~~~~~~~~~~~~
:Description:
E-mail domains whitelist is used to specify allowed email address
domains. If the list is non-empty and a user attempts registration
using an email address belonging to a domain that is not on the
list, registration will be enied. This is a more restrictive
option than <blacklist_file>, and therefore, in case
<whitelist_file> is defined, <blacklist_file> will be ignored.
:Default: ``disposable_email_whitelist.conf``
:Type: str
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``registration_warning_message``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -3265,7 +3280,9 @@
:Description:
XML config file that contains the job metric collection
configuration.
:Default: ``config/job_metrics_conf.xml``
The value of this option will be resolved with respect to
<config_dir>.
:Default: ``job_metrics_conf.xml``
:Type: str
@@ -115,7 +115,7 @@ Some **example test InteractiveTools** have been defined, and can be added to th
.. code-block:: xml
<tool file="../test/functional/tools/interactive_juypter_notebook.xml" />
<tool file="../test/functional/tools/interactive_cellxgene.xml" />
<tool file="interactive/interactivetool_jupyter_notebook.xml" />
<tool file="interactive/interactivetool_cellxgene.xml" />
+98 -126
View File
@@ -100,11 +100,24 @@ LOGGING_CONFIG_DEFAULT = {
def find_root(kwargs):
root = os.path.abspath(kwargs.get('root_dir', '.'))
return root
return os.path.abspath(kwargs.get('root_dir', '.'))
class BaseAppConfiguration(object):
# Override in subclasses (optional): {KEY: config option, VALUE: deprecated directory name}
# If VALUE == first directory in a user-supplied path that resolves to KEY, it will be stripped from that path
deprecated_dirs = None
def __init__(self, **kwargs):
self.config_dict = kwargs
self.root = find_root(kwargs)
self._set_config_base(kwargs)
self.schema = self._load_schema() # Load schema from schema definition file
self._raw_config = self.schema.defaults.copy() # Save schema defaults as initial config values (raw_config)
self._update_raw_config_from_kwargs(kwargs) # Overwrite raw_config with values passed in kwargs
self._create_attributes_from_raw_config() # Create attributes based on raw_config
self._resolve_paths() # Overwrite attribute values with resolved paths
def _set_config_base(self, config_kwargs):
def _set_global_conf():
@@ -163,6 +176,75 @@ class BaseAppConfiguration(object):
_set_global_conf()
_set_config_directories()
def _load_schema(self):
# Override in subclasses
raise Exception('Not implemented')
def _update_raw_config_from_kwargs(self, kwargs):
def convert_datatype(key, value):
datatype = self.schema.app_schema[key].get('type')
# check for `not None` explicitly (value can be falsy)
if value is not None and datatype in type_converters:
return type_converters[datatype](value)
return value
def strip_deprecated_dir(key, value):
resolves_to = self.schema.paths_to_resolve.get(key)
if resolves_to: # value is a path that will be resolved
first_dir = value.split(os.sep)[0] # get first directory component
if first_dir == self.deprecated_dirs.get(resolves_to): # first_dir is deprecated for this option
ignore = first_dir + os.sep
log.warning(
"Paths for the '%s' option are now relative to '%s', remove the leading '%s' "
"to suppress this warning: %s", key, resolves_to, ignore, value
)
return value[len(ignore):]
return value
type_converters = {'bool': string_as_bool, 'int': int, 'float': float, 'str': str}
for key, value in kwargs.items():
if key in self.schema.app_schema:
value = convert_datatype(key, value)
if value and self.deprecated_dirs:
value = strip_deprecated_dir(key, value)
self._raw_config[key] = value
def _create_attributes_from_raw_config(self):
# `base_configs` are a special case: these attributes have been created and will be ignored
# by the code below. Trying to overwrite any other existing attributes will raise an error.
base_configs = {'config_dir', 'data_dir', 'managed_config_dir'}
for key, value in self._raw_config.items():
if not hasattr(self, key):
setattr(self, key, value)
elif key not in base_configs:
raise ConfigurationError("Attempting to override existing attribute '%s'" % key)
def _resolve_paths(self):
def resolve(key):
if key in _cache: # resolve each path only once
return _cache[key]
path = getattr(self, key) # path prior to being resolved
parent = self.schema.paths_to_resolve.get(key)
if not parent: # base case: nothing else needs resolving
return path
parent_path = resolve(parent) # recursively resolve parent path
if path is not None:
path = os.path.join(parent_path, path) # resolve path
else:
path = parent_path # or use parent path
setattr(self, key, path) # update property
_cache[key] = path # cache it!
return path
_cache = {}
for key in self.schema.paths_to_resolve:
resolve(key)
def _in_managed_config_dir(self, path):
return os.path.join(self.managed_config_dir, path)
@@ -212,133 +294,14 @@ class BaseAppConfiguration(object):
class GalaxyAppConfiguration(BaseAppConfiguration):
deprecated_options = ('database_file', 'track_jobs_in_database')
default_config_file_name = 'galaxy.yml'
# {key: config option, value: deprecated directory name}
# If value == first dir in a user path that resolves to key, it will be stripped from the path
deprecated_dirs = {'config_dir': 'config', 'data_dir': 'database'}
def __init__(self, **kwargs):
self._load_schema() # Load schema from schema definition file
self._load_config_from_schema() # Load default propery values from schema
self._validate_schema_paths() # check that paths can be resolved
self._update_raw_config_from_kwargs(kwargs) # Overwrite default values passed as kwargs
self._create_attributes_from_raw_config() # Create attributes for LOADED properties
self.config_dict = kwargs
self.root = find_root(kwargs)
self._set_config_base(kwargs) # must be called prior to _resolve_paths()
self._resolve_paths(kwargs) # Overwrite attributes (not _raw_config) w/resolved paths
self._process_config(kwargs) # Finish processing configuration
super(GalaxyAppConfiguration, self).__init__(**kwargs)
self._process_config(kwargs)
def _load_schema(self):
self.schema = AppSchema(GALAXY_CONFIG_SCHEMA_PATH, GALAXY_APP_NAME)
self.appschema = self.schema.app_schema
def _load_config_from_schema(self):
self._raw_config = {} # keeps track of startup values (kwargs or schema default)
self.reloadable_options = set() # config options we can reload at runtime
self._paths_to_resolve = {} # {config option: referenced config option}
for key, data in self.appschema.items():
self._raw_config[key] = data.get('default')
if data.get('reloadable'):
self.reloadable_options.add(key)
if data.get('path_resolves_to'):
self._paths_to_resolve[key] = data.get('path_resolves_to')
def _validate_schema_paths(self):
def check_exists(option, key):
if not option:
message = "Invalid schema: property '{}' listed as path resolution target " \
"for '{}' does not exist".format(resolves_to, key)
raise_error(message)
def check_type_is_str(option, key):
if option.get('type') != 'str':
message = "Invalid schema: property '{}' should have type 'str'".format(key)
raise_error(message)
def check_is_dag():
visited = set()
for key in self._paths_to_resolve:
visited.clear()
while key:
visited.add(key)
key = self.appschema[key].get('path_resolves_to')
if key and key in visited:
raise_error('Invalid schema: cycle detected')
def raise_error(message):
log.error(message)
raise ConfigurationError(message)
for key, resolves_to in self._paths_to_resolve.items():
parent = self.appschema.get(resolves_to)
check_exists(parent, key)
check_type_is_str(parent, key)
check_type_is_str(self.appschema[key], key)
check_is_dag() # must be called last: walks entire graph
def _update_raw_config_from_kwargs(self, kwargs):
def convert_datatype(key, value):
datatype = self.appschema[key].get('type')
# check for `not None` explicitly (value can be falsy)
if value is not None and datatype in type_converters:
return type_converters[datatype](value)
return value
def strip_deprecated_dir(key, value):
resolves_to = self.appschema[key].get('path_resolves_to')
if resolves_to: # value is a path that will be resolved
first_dir = value.split(os.sep)[0] # get first directory component
if first_dir == self.deprecated_dirs[resolves_to]: # first_dir is deprecated for this option
ignore = first_dir + os.sep
log.warning(
"Paths for the '%s' option are now relative to '%s', remove the leading '%s' "
"to suppress this warning: %s", key, resolves_to, ignore, value
)
return value[len(ignore):]
return value
type_converters = {'bool': string_as_bool, 'int': int, 'float': float, 'str': str}
for key, value in kwargs.items():
if key in self.appschema:
value = convert_datatype(key, value)
if value:
value = strip_deprecated_dir(key, value)
self._raw_config[key] = value
def _create_attributes_from_raw_config(self):
for key, value in self._raw_config.items():
if hasattr(self, key):
raise ConfigurationError("Attempting to override existing attribute '%s'" % key)
setattr(self, key, value)
def _resolve_paths(self, kwargs):
def resolve(key):
if key in _cache: # resolve each path only once
return _cache[key]
path = getattr(self, key) # path prior to being resolved
parent = self.appschema[key].get('path_resolves_to')
if not parent: # base case: nothing else needs resolving
return path
parent_path = resolve(parent) # recursively resolve parent path
if path is not None:
path = os.path.join(parent_path, path) # resolve path
else:
path = parent_path # or use parent path
setattr(self, key, path) # update property
_cache[key] = path # cache it!
return path
_cache = {}
for key in self._paths_to_resolve:
resolve(key)
return AppSchema(GALAXY_CONFIG_SCHEMA_PATH, GALAXY_APP_NAME)
def _process_config(self, kwargs):
# Resolve paths of other config files
@@ -465,6 +428,15 @@ class GalaxyAppConfiguration(BaseAppConfiguration):
except IOError:
log.error("CONFIGURATION ERROR: Can't open supplied blacklist file from path: %s", self.blacklist_file)
# Create whitelist file to accept only certain email domains
self.whitelist_content = None
if self.whitelist_file:
try:
with open(self.whitelist_file) as f:
self.whitelist_content = [line.rstrip() for line in f]
except IOError:
log.error("CONFIGURATION ERROR: Can't open supplied whitelist file from path: %s", self.whitelist_file)
self.persistent_communication_rooms = listify(self.persistent_communication_rooms, do_strip=True)
# The transfer manager and deferred job queue
self.enable_beta_job_managers = string_as_bool(kwargs.get('enable_beta_job_managers', 'False'))
@@ -526,7 +498,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration):
if self.tool_dependency_dir and self.tool_dependency_dir.lower() == 'none':
self.tool_dependency_dir = None
if self.involucro_path is None:
target_dir = self.tool_dependency_dir or self.appschema['tool_dependency_dir'].get('default')
target_dir = self.tool_dependency_dir or self.schema.defaults['tool_dependency_dir']
self.involucro_path = os.path.join(self.data_dir, target_dir, "involucro")
self.involucro_path = os.path.join(self.root, self.involucro_path)
if self.mulled_channels:
@@ -897,7 +869,7 @@ Configuration = GalaxyAppConfiguration
def reload_config_options(current_config):
""" Reload modified reloadable config options """
modified_config = read_properties_from_file(current_config.config_file)
for option in current_config.reloadable_options:
for option in current_config.schema.reloadable_options:
if option in modified_config:
# compare to raw value, as that one is set only on load and reload
if current_config._raw_config[option] != modified_config[option]:
+1 -1
View File
@@ -30,12 +30,12 @@ if __name__ == '__main__':
from galaxy.config import GALAXY_CONFIG_SCHEMA_PATH
from galaxy.config.schema import (
AppSchema,
OPTION_DEFAULTS,
Schema,
)
from galaxy.util import safe_makedirs
from galaxy.util.properties import nice_config_parser
from galaxy.util.yaml_util import (
OPTION_DEFAULTS,
ordered_dump,
ordered_load,
)
+11 -1
View File
@@ -687,6 +687,14 @@ galaxy:
# Example value 'config/disposable_email_blacklist.conf'
#blacklist_file: null
# E-mail domains whitelist is used to specify allowed email address
# domains. If the list is non-empty and a user attempts registration
# using an email address belonging to a domain that is not on the
# list, registration will be enied. This is a more restrictive option
# than <blacklist_file>, and therefore, in case <whitelist_file> is
# defined, <blacklist_file> will be ignored.
#whitelist_file: disposable_email_whitelist.conf
# Registration warning message is used to discourage people from
# registering multiple accounts. Applies mostly for the main Galaxy
# instance. If no message specified the warning box will not be shown.
@@ -1607,7 +1615,9 @@ galaxy:
# XML config file that contains the job metric collection
# configuration.
#job_metrics_config_file: config/job_metrics_conf.xml
# The value of this option will be resolved with respect to
# <config_dir>.
#job_metrics_config_file: job_metrics_conf.xml
# This option allows users to see the job metrics (except for
# environment variables).
@@ -344,6 +344,14 @@ tool_shed:
# the registration.
#blacklist_file: config/disposable_email_blacklist.conf
# E-mail domains whitelist is used to specify allowed email address
# domains. If the list is non-empty and a user attempts registration
# using an email address belonging to a domain that is not on the
# list, registration will be enied. This is a more restrictive option
# than <blacklist_file>, and therefore, in case <whitelist_file> is
# defined, <blacklist_file> will be ignored.
#whitelist_file: disposable_email_whitelist.conf
# Append "/{brand}" to the "Galaxy" text in the masthead.
#brand: null
+74 -4
View File
@@ -1,8 +1,16 @@
from galaxy.util.yaml_util import (
OPTION_DEFAULTS,
ordered_load,
)
import logging
from galaxy.exceptions import ConfigurationError
from galaxy.util.yaml_util import ordered_load
log = logging.getLogger(__name__)
OPTION_DEFAULTS = {
"type": "str",
"unknown_option": False,
"default": None,
"desc": None,
}
UNKNOWN_OPTION = {
"type": "str",
@@ -33,8 +41,70 @@ class AppSchema(Schema):
self.raw_schema = self._read_schema(schema_path)
self.description = self.raw_schema.get("desc", None)
app_schema = self.raw_schema['mapping'][app_name]['mapping']
self._preprocess(app_schema)
super(AppSchema, self).__init__(app_schema)
def _read_schema(self, path):
with open(path, "r") as f:
return ordered_load(f)
def _preprocess(self, app_schema):
"""Populate schema collections used for app configuration."""
self._defaults = {} # {config option: default value or null}
self._reloadable_options = set() # config options we can reload at runtime
self._paths_to_resolve = {} # {config option: referenced config option}
for key, data in app_schema.items():
self._defaults[key] = data.get('default')
if data.get('reloadable'):
self._reloadable_options.add(key)
if data.get('path_resolves_to'):
self._paths_to_resolve[key] = data.get('path_resolves_to')
@property
def defaults(self):
return self._defaults
@property
def paths_to_resolve(self):
return self._paths_to_resolve
@property
def reloadable_options(self):
return self._reloadable_options
def validate_path_resolution_graph(self):
"""This method is for tests only: we SHOULD validate the schema's path resolution graph
as part of automated testing; but we should NOT validate it at runtime.
"""
def check_exists(option, key):
if not option:
message = "Invalid schema: property '{}' listed as path resolution target " \
"for '{}' does not exist".format(resolves_to, key)
raise_error(message)
def check_type_is_str(option, key):
if option.get('type') != 'str':
message = "Invalid schema: property '{}' should have type 'str'".format(key)
raise_error(message)
def check_is_dag():
visited = set()
for key in self.paths_to_resolve:
visited.clear()
while key:
visited.add(key)
key = self.app_schema[key].get('path_resolves_to')
if key and key in visited:
raise_error('Invalid schema: cycle detected')
def raise_error(message):
log.error(message)
raise ConfigurationError(message)
for key, resolves_to in self.paths_to_resolve.items():
print(key)
parent = self.app_schema.get(resolves_to)
check_exists(parent, key)
check_type_is_str(parent, key)
check_type_is_str(self.app_schema[key], key)
check_is_dag() # must be called last: walks entire graph
@@ -78,6 +78,7 @@ def condor_submit(submit_file):
try:
submit = Popen(('condor_submit', submit_file), stdout=PIPE, stderr=STDOUT)
message, _ = submit.communicate()
message = unicodify(message)
if submit.returncode == 0:
external_id = parse_external_id(message, type='condor')
else:
+1 -2
View File
@@ -658,8 +658,7 @@ class GalaxyRBACAgent(RBACAgent):
def create_user_role(self, user, app):
# Create private user role if necessary
if not self.get_private_user_role(user):
self.create_private_user_role(user)
self.get_private_user_role(user, auto_create=True)
# Create default user permissions if necessary
if not user.default_permissions:
if hasattr(app.config, "new_user_dataset_access_role_default_private"):
@@ -39,6 +39,13 @@ def validate_email(trans, email, user=None, check_dup=True, allow_empty=False):
message = "Email address cannot be more than %d characters in length." % EMAIL_MAX_LEN
elif check_dup and trans.sa_session.query(trans.app.model.User).filter(func.lower(trans.app.model.User.table.c.email) == email.lower()).first():
message = "User with email '%s' already exists." % email
# If the whitelist is not empty filter out any domain not in the list and ignore blacklist.
elif trans.app.config.whitelist_content is not None:
domain = email.split('@')[1]
if len(domain.split('.')) > 2:
domain = ('.').join(domain.split('.')[-2:])
if domain not in trans.app.config.whitelist_content:
message = "Please enter an allowed domain email address for this server."
# If the blacklist is not empty filter out the disposable domains.
elif trans.app.config.blacklist_content is not None:
domain = email.split('@')[1]
+3 -2
View File
@@ -208,8 +208,9 @@ class NavigatesGalaxy(HasDriver):
return self.history_panel_name_element().text
def current_history(self):
history = self.api_get("histories")[0]
return history
full_url = self.build_url("history/current_history_json", for_selenium=False)
response = requests.get(full_url, cookies=self.selenium_to_requests_cookies())
return response.json()
def current_history_id(self):
return self.current_history()["id"]
+38 -1
View File
@@ -60,6 +60,7 @@ masthead:
# user menu
logout: 'Logout'
custom_builds: 'Custom Builds'
preferences: 'Preferences'
histories: 'Histories'
pages: 'Pages'
@@ -67,6 +68,21 @@ masthead:
libraries: 'Data Libraries'
published_histories: 'Histories'
preferences:
selectors:
manage_information: '#edit-preferences-information'
current_email: "#user-preferences-current-email"
change_user_email:
selectors:
submit: '#submit'
change_user_address:
selectors:
address_button:
type: xpath
selector: '//span[contains(text(), "Insert Address")]'
history_panel:
menu:
labels:
@@ -172,9 +188,30 @@ multi_history_view:
selectors:
_: '.multi-panel-history'
histories: '.middle .history-column'
current_label: '.current-label'
create_new_button: '.create-new'
drag_drop_help: '.history-drop-target-help'
switch_history: '.switch-to'
history_dropdown_btn: '[history-dropdown-btn="${history_id}"]'
copy: '.copy-history'
delete: '.copy-history'
current_history_check: '#history-column-${history_id} .current-label'
empty_message_check: '.empty-message'
history_dropdown_menu:
selectors:
_: '[history-dropdown-menu="${history_id}"]'
delete: '[history-dropdown-menu="${history_id}"] > .delete-history'
purge: '[history-dropdown-menu="${history_id}"] > .purge-history'
copy_history_modal:
selectors:
_: '.modal-dialog'
copy_btn:
type: xpath
selector: '//button[contains(text(), "Copy")]'
history_copy_elements:
@@ -294,7 +331,7 @@ workflow_editor:
label_input: "[tour_id='__label'] input"
annotation_input: "[tour_id='__annotation'] textarea"
connector_for: "canvas[handle1-id='${source_id}'][handle2-id='${sink_id}']"
connector_for: "div[handle1-id='${source_id}'][handle2-id='${sink_id}']"
connector_destroy_callout: '.delete-terminal'
+4 -3
View File
@@ -1217,9 +1217,10 @@ class BaseGalaxyToolBox(AbstractToolBox):
return
app_config_dict = self.app.config.config_dict
conf_file = app_config_dict.get("dependency_resolvers_config_file")
default_tool_dependency_dir = os.path.join(self.app.config.data_dir, self.app.config.appschema['tool_dependency_dir'].get('default'))
self.dependency_manager = build_dependency_manager(app_config_dict=app_config_dict, conf_file=conf_file,
default_tool_dependency_dir=default_tool_dependency_dir)
default_tool_dependency_dir = os.path.join(
self.app.config.data_dir, self.app.config.schema.defaults['tool_dependency_dir'])
self.dependency_manager = build_dependency_manager(
app_config_dict=app_config_dict, conf_file=conf_file, default_tool_dependency_dir=default_tool_dependency_dir)
def reload_dependency_manager(self):
self._init_dependency_manager()
-7
View File
@@ -10,13 +10,6 @@ from yaml.constructor import ConstructorError
log = logging.getLogger(__name__)
OPTION_DEFAULTS = {
"type": "str",
"unknown_option": False,
"default": None,
"desc": None,
}
class OrderedLoader(yaml.SafeLoader):
# This class was pulled out of ordered_load() for the sake of
+13 -1
View File
@@ -902,6 +902,18 @@ mapping:
Example value 'config/disposable_email_blacklist.conf'
whitelist_file:
type: str
default: disposable_email_whitelist.conf
path_resolves_to: config_dir
required: false
desc: |
E-mail domains whitelist is used to specify allowed email address domains.
If the list is non-empty and a user attempts registration using an email
address belonging to a domain that is not on the list, registration will be
enied. This is a more restrictive option than <blacklist_file>, and therefore,
in case <whitelist_file> is defined, <blacklist_file> will be ignored.
registration_warning_message:
type: str
default: >-
@@ -2414,7 +2426,7 @@ mapping:
required: false
desc: |
XML config file that contains the job metric collection configuration.
The value of this option will be resolved with respect to <config_dir>.
expose_potentially_sensitive_job_metrics:
@@ -1,3 +1,5 @@
from selenium.common.exceptions import NoSuchElementException
from .framework import (
selenium_test,
SeleniumTestCase
@@ -27,15 +29,8 @@ class HistoryMultiViewTestCase(SeleniumTestCase):
@selenium_test
def test_list_list_display(self):
history_id = self.current_history_id()
collection = self.dataset_collection_populator.create_list_of_list_in_history(history_id).json()
collection_hid = collection["hid"]
self.home()
self.components.history_panel.multi_view_button.wait_for_and_click()
selector = self.history_panel_wait_for_hid_state(collection_hid, "ok")
self.click(selector)
method = self.dataset_collection_populator.create_list_of_list_in_history(history_id).json
selector = self.prepare_multi_history_view(method)
first_level_element_selector = selector.descendant(".dataset-collection-element")
self.wait_for_and_click(first_level_element_selector)
dataset_selector = first_level_element_selector.descendant(".dataset")
@@ -47,15 +42,9 @@ class HistoryMultiViewTestCase(SeleniumTestCase):
@selenium_test
def test_list_list_list_display(self):
history_id = self.current_history_id()
collection = self.dataset_collection_populator.create_nested_collection(history_id, collection_type="list:list:list").json()
collection_hid = collection["hid"]
method = self.dataset_collection_populator.create_nested_collection(history_id, collection_type="list:list:list").json
selector = self.prepare_multi_history_view(method)
self.home()
self.components.history_panel.multi_view_button.wait_for_and_click()
selector = self.history_panel_wait_for_hid_state(collection_hid, "ok")
self.click(selector)
first_level_element_selector = selector.descendant(".dataset-collection-element")
self.wait_for_and_click(first_level_element_selector)
second_level_element_selector = first_level_element_selector.descendant(".dataset-collection-element")
@@ -65,3 +54,101 @@ class HistoryMultiViewTestCase(SeleniumTestCase):
self.sleep_for(self.wait_types.UX_TRANSITION)
self.screenshot("multi_history_list_list_list")
@selenium_test
def test_copy_history(self):
history_id = self.current_history_id()
method = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json
self.prepare_multi_history_view(method)
self.copy_history(history_id)
self.assert_history(history_id, histories_number=2)
@selenium_test
def test_delete_history(self):
history_id = self.current_history_id()
method = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json
self.prepare_multi_history_view(method)
self.copy_history(history_id)
self.components.multi_history_view.history_dropdown_btn(history_id=history_id).wait_for_and_click()
# click on delete button with corresponding history_id
self.components.multi_history_view.history_dropdown_menu.delete(history_id=history_id).wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_history(history_id, should_exist=False)
@selenium_test
def test_purge_history(self):
history_id = self.current_history_id()
method = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json
self.prepare_multi_history_view(method)
self.copy_history(history_id)
self.components.multi_history_view.history_dropdown_btn(history_id=history_id).wait_for_and_click()
# click on purge button with corresponding history_id
self.components.multi_history_view.history_dropdown_menu.purge(history_id=history_id).wait_for_and_click()
self.driver.switch_to_alert().accept()
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_history(history_id, should_exist=False)
@selenium_test
def test_switching_history(self):
history_id = self.current_history_id()
method = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json
self.prepare_multi_history_view(method)
self.copy_history(history_id)
self.components.multi_history_view.switch_history.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_history(history_id, histories_number=2)
# assert that id of current history equals history_id
assert self.components.multi_history_view.current_history_check(history_id=history_id).is_displayed
self.components.multi_history_view.switch_history.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
# assert that 'history_id' is not current history
self.assertRaises(NoSuchElementException, lambda:
self.components.multi_history_view.current_history_check(history_id=history_id).is_displayed)
@selenium_test
def test_create_new_history(self):
history_id = self.current_history_id()
method = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json
self.prepare_multi_history_view(method)
# assert that empty history is not created in advance
self.components.multi_history_view.empty_message_check.assert_absent_or_hidden()
self.components.multi_history_view.create_new_button.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_history(history_id, histories_number=2)
self.sleep_for(self.wait_types.UX_RENDER)
# assert that empty history is present
self.components.multi_history_view.empty_message_check.wait_for_present()
def assert_history(self, history_id, histories_number=1, should_exist=True):
histories = self.components.multi_history_view.histories.all()
assert len(histories) == histories_number
# search for history with history_id
assert should_exist == any(history.get_attribute("id") == "history-column-" + history_id for history in histories)
def copy_history(self, history_id):
self.components.multi_history_view.history_dropdown_btn(history_id=history_id).wait_for_and_click()
self.components.multi_history_view.copy.wait_for_and_click()
self.components.multi_history_view.copy_history_modal.copy_btn.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
def prepare_multi_history_view(self, collection_populator_method):
collection = collection_populator_method()
collection_hid = collection["hid"]
self.home()
self.components.history_panel.multi_view_button.wait_for_and_click()
selector = self.history_panel_wait_for_hid_state(collection_hid, "ok")
self.click(selector)
return selector
@@ -0,0 +1,111 @@
from .framework import (
selenium_test,
SeleniumTestCase,
)
class ManageInformationTestCase(SeleniumTestCase):
@selenium_test
def test_change_email(self):
def assert_email(email_to_check):
self.assertTrue(email_to_check == self.driver.find_element_by_id("user-preferences-current-email").text)
email = self._get_random_email()
self.register(email)
self.navigate_to_user_preferences()
self.sleep_for(self.wait_types.UX_RENDER)
# rendered email should be equal
assert_email(email)
self.components.preferences.manage_information.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
new_email = self._get_random_email()
# new email should be different from initially registered
self.assertTrue(email != new_email)
email_input_field = self.driver.find_element_by_css_selector("[tour_id='email'] input")
self.clear_input_field_and_write(email_input_field, new_email)
self.components.change_user_email.submit.wait_for_and_click()
# UX_RENDER time sometimes is not enough
self.sleep_for(self.wait_types.UX_TRANSITION)
# email should be changed
assert_email(new_email)
@selenium_test
def test_public_name(self):
def get_name_input_field():
return self.driver.find_element_by_css_selector("[tour_id='username'] input")
def assert_public_name(expected_name):
self.assertTrue(expected_name == get_name_input_field().get_attribute('value'))
public_name = "user-public-name"
self.register(username=public_name)
self.navigate_to_manage_information()
self.sleep_for(self.wait_types.UX_RENDER)
# make sure that public name is the same as registered
assert_public_name(public_name)
new_public_name = "new-public-name"
# change public name
self.clear_input_field_and_write(get_name_input_field(), new_public_name)
self.components.change_user_email.submit.wait_for_and_click()
self.sleep_for(self.wait_types.UX_TRANSITION)
self.navigate_to_manage_information()
self.sleep_for(self.wait_types.UX_RENDER)
# public name field should render new public name
assert_public_name(new_public_name)
@selenium_test
def test_user_address(self):
def get_address_form():
return self.driver.find_element_by_css_selector("div.ui-portlet-section > div.portlet-content")
self.register(self._get_random_email())
self.navigate_to_manage_information()
self.components.change_user_address.address_button.wait_for_and_click()
address_field_labels = ["Short address description", "Name", "Institution", "Address", "City",
"State/Province/Region", "Postal Code", "Country", "Phone"]
address_fields = {}
# fill address fields with random data
for input_field_label in address_field_labels:
input_value = self._get_random_name(prefix=input_field_label)
address_fields[input_field_label] = input_value
input_field = self.get_address_input_field(get_address_form(), input_field_label)
self.clear_input_field_and_write(input_field, input_value)
# save new address
self.components.change_user_email.submit.wait_for_and_click()
self.sleep_for(self.wait_types.UX_TRANSITION)
self.navigate_to_manage_information()
self.sleep_for(self.wait_types.UX_RENDER)
# check if address was saved correctly
for input_field_label in address_fields.keys():
input_field = self.get_address_input_field(get_address_form(), input_field_label)
self.assertTrue(input_field.get_attribute('value') == address_fields[input_field_label])
def navigate_to_user_preferences(self):
self.home()
self.click_masthead_user()
self.components.masthead.preferences.wait_for_and_click()
def navigate_to_manage_information(self):
self.navigate_to_user_preferences()
self.components.preferences.manage_information.wait_for_and_click()
def clear_input_field_and_write(self, element, new_input_text):
element.clear()
element.send_keys(new_input_text)
def get_address_input_field(self, address_form, input_field_label):
return address_form.find_element_by_css_selector("[data-label='" + input_field_label + "'] > input")
+16
View File
@@ -10,6 +10,7 @@ from datetime import timedelta
from six.moves import configparser
from galaxy.config import BaseAppConfiguration
from galaxy.config.schema import AppSchema
from galaxy.util import string_as_bool
from galaxy.version import VERSION, VERSION_MAJOR
from galaxy.web.formatting import expand_pretty_datetime_format
@@ -19,6 +20,9 @@ log = logging.getLogger(__name__)
ts_webapp_path = os.path.abspath(os.path.dirname(__file__))
templates_path = os.path.join(ts_webapp_path, "templates")
TOOLSHED_APP_NAME = 'tool_shed'
TOOLSHED_CONFIG_SCHEMA_PATH = 'lib/tool_shed/webapp/config_schema.yml'
def resolve_path(path, root):
"""If 'path' is relative make absolute by prepending 'root'"""
@@ -34,6 +38,18 @@ class ConfigurationError(Exception):
class ToolShedAppConfiguration(BaseAppConfiguration):
default_config_file_name = 'tool_shed.yml'
def _load_schema(self):
return AppSchema(TOOLSHED_CONFIG_SCHEMA_PATH, TOOLSHED_APP_NAME)
def _update_raw_config_from_kwargs(self, kwargs):
pass # Intentionally does nothing. To enable, remove + call base __init__().
def _create_attributes_from_raw_config(self):
pass # Intentionally does nothing. To enable, remove + call base __init__().
def _resolve_paths(self, paths_to_resolve):
pass # Intentionally does nothing. To enable, remove + call base __init__().
def __init__(self, **kwargs):
self.config_dict = kwargs
self.root = kwargs.get('root_dir', '.')
+12
View File
@@ -482,6 +482,18 @@ mapping:
disposable email address during the registration. If their address domain
matches any domain in the blacklist, they are refused the registration.
whitelist_file:
type: str
default: disposable_email_whitelist.conf
path_resolves_to: config_dir
required: false
desc: |
E-mail domains whitelist is used to specify allowed email address domains.
If the list is non-empty and a user attempts registration using an email
address belonging to a domain that is not on the list, registration will be
enied. This is a more restrictive option than <blacklist_file>, and therefore,
in case <whitelist_file> is defined, <blacklist_file> will be ignored.
brand:
type: str
default: null
+1 -1
View File
@@ -254,7 +254,7 @@ def get_config_data():
create_driver() # create + setup DRIVER
parent_dirs = load_parent_dirs() # called after DRIVER is setup
items = ((k, v) for k, v in DRIVER.app.config.appschema.items() if k not in DO_NOT_TEST)
items = ((k, v) for k, v in DRIVER.app.config.schema.app_schema.items() if k not in DO_NOT_TEST)
for key, data in items:
expected_value = get_expected(key, data, parent_dirs)
loaded_value = getattr(DRIVER.app.config, key)
+9
View File
@@ -0,0 +1,9 @@
from galaxy_test.driver import integration_util
class ConfigSchemaTestCase(integration_util.IntegrationTestCase):
def test_schema_path_resolution_graph(self):
# Run schema's validation method; throws error if schema invalid
schema = self._app.config.schema
schema.validate_path_resolution_graph()
+8 -10
View File
@@ -1,9 +1,10 @@
import pytest
from galaxy.config import GalaxyAppConfiguration
from galaxy.config.schema import AppSchema
MOCK_PROPERTIES = {
MOCK_SCHEMA = {
'property1': {'default': 'a', 'type': 'str'}, # str
'property2': {'default': 1, 'type': 'int'}, # int
'property3': {'default': 1.0, 'type': 'float'}, # float
@@ -13,17 +14,14 @@ MOCK_PROPERTIES = {
}
def get_schema(app_mapping):
return {'mapping': {'galaxy': {'mapping': app_mapping}}}
@pytest.fixture
def mock_init(monkeypatch):
def mock_load_schema(self):
self.appschema = MOCK_PROPERTIES
def mock_process_config(self, kwargs):
pass
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', mock_process_config)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(MOCK_SCHEMA))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
def test_load_config_from_schema(mock_init):
+21 -45
View File
@@ -1,6 +1,7 @@
import pytest
from galaxy.config import GalaxyAppConfiguration
from galaxy.config.schema import AppSchema
from galaxy.exceptions import ConfigurationError
@@ -15,14 +16,11 @@ from galaxy.exceptions import ConfigurationError
# whereas an invalid configuration raises an error.
@pytest.fixture
def mock_config(monkeypatch):
def mock_process_config(self, kwargs):
pass
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', mock_process_config)
def get_schema(app_mapping):
return {'mapping': {'galaxy': {'mapping': app_mapping}}}
def test_basecase(mock_config, monkeypatch):
def test_basecase(monkeypatch):
# Check that a valid graph is loaded correctly (this graph has 2 components)
mock_schema = {
'component1_path0': {
@@ -48,12 +46,9 @@ def test_basecase(mock_config, monkeypatch):
'default': 'value4',
'path_resolves_to': 'component2_path0',
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
config = GalaxyAppConfiguration()
assert config.component1_path0 == 'value0'
@@ -63,7 +58,7 @@ def test_basecase(mock_config, monkeypatch):
assert config.component2_path1 == 'value3/value4'
def test_resolves_to_invalid_property(mock_config, monkeypatch):
def test_resolves_to_invalid_property(monkeypatch):
# 'path_resolves_to' should point to an existing property in the schema
mock_schema = {
'path0': {
@@ -76,17 +71,13 @@ def test_resolves_to_invalid_property(mock_config, monkeypatch):
'path_resolves_to': 'invalid', # invalid
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
with pytest.raises(ConfigurationError):
GalaxyAppConfiguration()
AppSchema(None, 'galaxy').validate_path_resolution_graph()
def test_path_resolution_cycle(mock_config, monkeypatch):
def test_path_resolution_cycle(monkeypatch):
# Must be a DAG, but this one has a cycle
mock_schema = {
'path0': {
@@ -105,17 +96,13 @@ def test_path_resolution_cycle(mock_config, monkeypatch):
'path_resolves_to': 'path1',
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
with pytest.raises(ConfigurationError):
GalaxyAppConfiguration()
AppSchema(None, 'galaxy').validate_path_resolution_graph()
def test_path_invalid_type(mock_config, monkeypatch):
def test_path_invalid_type(monkeypatch):
# Paths should be strings
mock_schema = {
'path0': {
@@ -128,17 +115,13 @@ def test_path_invalid_type(mock_config, monkeypatch):
'path_resolves_to': 'path0',
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
with pytest.raises(ConfigurationError):
GalaxyAppConfiguration()
AppSchema(None, 'galaxy').validate_path_resolution_graph()
def test_resolves_to_invalid_type(mock_config, monkeypatch):
def test_resolves_to_invalid_type(monkeypatch):
# Paths should be strings
mock_schema = {
'path0': {
@@ -151,17 +134,13 @@ def test_resolves_to_invalid_type(mock_config, monkeypatch):
'path_resolves_to': 'path0',
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
with pytest.raises(ConfigurationError):
GalaxyAppConfiguration()
AppSchema(None, 'galaxy').validate_path_resolution_graph()
def test_resolves_with_empty_component(mock_config, monkeypatch):
def test_resolves_with_empty_component(monkeypatch):
# A path can be None (root path is never None; may be asigned elsewhere)
mock_schema = {
'path0': {
@@ -178,11 +157,8 @@ def test_resolves_with_empty_component(mock_config, monkeypatch):
'path_resolves_to': 'path1',
},
}
def mock_load_schema(self):
self.appschema = mock_schema
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(mock_schema))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
config = GalaxyAppConfiguration()
assert config.path0 == 'value0'
+10 -20
View File
@@ -1,6 +1,7 @@
import pytest
from galaxy.config import GalaxyAppConfiguration
from galaxy.config.schema import AppSchema
MOCK_DEPRECATED_DIRS = {
@@ -35,34 +36,23 @@ MOCK_SCHEMA = {
}
def get_schema(app_mapping):
return {'mapping': {'galaxy': {'mapping': app_mapping}}}
def test_deprecated_prefixes_set_correctly(monkeypatch):
# Before we mock them, check that correct values are assigned
def mock_load_schema(self):
self.appschema = {}
def mock_process_config(self, kwargs):
pass
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', mock_process_config)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(MOCK_SCHEMA))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
config = GalaxyAppConfiguration()
expected_deprecated_dirs = {'config_dir': 'config', 'data_dir': 'database'}
assert config.deprecated_dirs == expected_deprecated_dirs
assert config.deprecated_dirs == {'config_dir': 'config', 'data_dir': 'database'}
@pytest.fixture
def mock_init(monkeypatch):
def mock_load_schema(self):
self.appschema = MOCK_SCHEMA
def mock_process_config(self, kwargs):
pass # because we're done before this is called
monkeypatch.setattr(GalaxyAppConfiguration, '_load_schema', mock_load_schema)
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', mock_process_config)
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(MOCK_SCHEMA))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
monkeypatch.setattr(GalaxyAppConfiguration, 'deprecated_dirs', MOCK_DEPRECATED_DIRS)
+33 -27
View File
@@ -1,33 +1,41 @@
import pytest
from galaxy import config
from galaxy.config import GalaxyAppConfiguration
from galaxy.config import reload_config_options
from galaxy.config.schema import AppSchema
R1, R2, N1, N2 = 'reloadable1', 'reloadable2', 'nonrelodable1', 'nonreloadable2' # config options
class MockGalaxyAppConfiguration():
def __init__(self, properties):
self.config_file = None
self.reloadable_options = {R1, R2}
self._raw_config = {}
for key, value in properties.items():
setattr(self, key, value)
self._raw_config[key] = value
def update_reloadable_property(self, key, value):
setattr(self, key, value)
MOCK_SCHEMA = {
R1: {'reloadable': True, 'default': 1},
R2: {'reloadable': True, 'default': 2},
N1: {'default': 3},
N2: {'default': 4},
}
def test_update_property(monkeypatch):
def get_schema(app_mapping):
return {'mapping': {'galaxy': {'mapping': app_mapping}}}
@pytest.fixture
def mock_init(monkeypatch):
monkeypatch.setattr(AppSchema, '_read_schema', lambda a, b: get_schema(MOCK_SCHEMA))
monkeypatch.setattr(GalaxyAppConfiguration, '_process_config', lambda a, b: None)
def test_update_property(mock_init, monkeypatch):
# This also covers adding a property. When a config file does not set a property,
# that property is set to its default value. Thus, if we add a reloadable property
# to the config file, it's the same as modifying that property's value.
appconfig = MockGalaxyAppConfiguration({R1: 1, R2: 2, N1: 3})
def mock_read_properties_from_file(values):
return {R1: 1, R2: 42, N1: 99} # edits: R2, N1 modified
# edits to config file: R2, N1 modified
monkeypatch.setattr(config, 'read_properties_from_file', lambda _: {R1: 1, R2: 42, N1: 99})
monkeypatch.setattr(config, 'read_properties_from_file', mock_read_properties_from_file)
appconfig = GalaxyAppConfiguration()
assert getattr(appconfig, R1) == 1
assert getattr(appconfig, R2) == 2
@@ -40,7 +48,7 @@ def test_update_property(monkeypatch):
assert getattr(appconfig, N1) == 3 # no change: option modified but is non-relodable
def test_overwrite_reloadable_attribute(monkeypatch):
def test_overwrite_reloadable_attribute(mock_init, monkeypatch):
# This is similar to test_update_property, but here we overwrite the attribute before reloading.
# This can happen if a config property is modified AFTER it has been loaded from schema or kwargs.
# For example: load `foo` (from schema or kwargs), but then, in a # subsequent step while initializing
@@ -48,12 +56,11 @@ def test_overwrite_reloadable_attribute(monkeypatch):
# is not what was initially loaded, and if `foo` is reloadable, it will be reset to its default as soon
# as the config file is modified. To prevent this, we compare the values read from the modified file
# to the `_raw_config` dict. This test ensures this works correctly.
appconfig = MockGalaxyAppConfiguration({R1: 1, R2: 2, N1: 3})
def mock_read_properties_from_file(values):
return {R1: 1, R2: 42} # edits: R2 modified
# edits to config file: R2 modified
monkeypatch.setattr(config, 'read_properties_from_file', lambda _: {R1: 1, R2: 42})
monkeypatch.setattr(config, 'read_properties_from_file', mock_read_properties_from_file)
appconfig = GalaxyAppConfiguration()
assert getattr(appconfig, R1) == 1
assert getattr(appconfig, R2) == 2
@@ -67,16 +74,15 @@ def test_overwrite_reloadable_attribute(monkeypatch):
assert getattr(appconfig, R2) == 42 # change: reloadable option modified
def test_cant_delete_property(monkeypatch):
def test_cant_delete_property(mock_init, monkeypatch):
# A property should not be deleted: we don't know whether it was initially
# set to a default, loaded from a config file, env var, etc. Therefore, if a property
# is removed from the config file, it will not be modified or deleted.
appconfig = MockGalaxyAppConfiguration({R1: 1, R2: 2, N1: 3, N2: 4})
def mock_read_properties_from_file(values):
return {R1: 1, N1: 3} # edits: R2, N2 deleted
# edits to config file: R2, N2 deleted
monkeypatch.setattr(config, 'read_properties_from_file', lambda _: {R1: 1, N1: 3})
monkeypatch.setattr(config, 'read_properties_from_file', mock_read_properties_from_file)
appconfig = GalaxyAppConfiguration()
assert getattr(appconfig, R1) == 1
assert getattr(appconfig, R2) == 2
+45 -8
View File
@@ -12,9 +12,27 @@ MOCK_YAML = '''
mockgalaxy:
type: map
mapping:
option:
attr1: a
attr2: b
property1:
default: a
type: str
path_resolves_to: foo
property2:
default: 1
type: int
reloadable: true
property3:
default: 1.0
type: float
reloadable: true
property4:
default: true
type: bool
path_resolves_to: foo
property5:
something_else: b
type: invalid
property6:
something_else: b
'''
@@ -29,10 +47,29 @@ def test_schema_is_loaded(monkeypatch):
monkeypatch.setattr(AppSchema, '_read_schema', mock_read_schema)
monkeypatch.setattr(OrderedLoader, '__init__', mock_init)
loaded_schema = AppSchema('no path', 'mockgalaxy')
schema = AppSchema('no path', 'mockgalaxy')
data = ordered_load(MOCK_YAML)
assert loaded_schema.description == data['desc']
assert loaded_schema.raw_schema['foo'] == 'bar'
assert loaded_schema.app_schema['option']['attr1'] == 'a'
assert loaded_schema.get_app_option('option')['attr2'] == 'b'
assert schema.description == data['desc']
assert schema.raw_schema['foo'] == 'bar'
assert len(schema.defaults) == 6
assert schema.defaults['property1'] == 'a'
assert schema.defaults['property2'] == 1
assert schema.defaults['property3'] == 1.0
assert schema.defaults['property4'] is True
assert schema.defaults['property5'] is None
assert schema.defaults['property6'] is None
assert type(schema.defaults['property1']) is str
assert type(schema.defaults['property2']) is int
assert type(schema.defaults['property3']) is float
assert type(schema.defaults['property4']) is bool
assert len(schema.reloadable_options) == 2
assert 'property2' in schema.reloadable_options
assert 'property3' in schema.reloadable_options
assert len(schema.paths_to_resolve) == 2
assert 'property1' in schema.paths_to_resolve
assert 'property4' in schema.paths_to_resolve
+1 -5
View File
@@ -564,11 +564,7 @@ class SimplifiedToolBox(ToolBox):
app.tool_cache = ToolCache() if not hasattr(app, 'tool_cache') else app.tool_cache
app.job_config.get_tool_resource_parameters = lambda tool_id: None
app.config.update_integrated_tool_panel = True
app.config.appschema = {
'tool_dependency_dir': {
'default': 'dependencies',
}
}
app.config.schema.defaults = {'tool_dependency_dir': 'dependencies'}
config_files = test_case.config_files
tool_root_dir = test_case.test_directory
super(SimplifiedToolBox, self).__init__(
+4
View File
@@ -111,6 +111,9 @@ class MockLock(object):
class MockAppConfig(Bunch):
class MockSchema(Bunch):
pass
def __init__(self, root=None, **kwargs):
Bunch.__init__(self, **kwargs)
if not root:
@@ -118,6 +121,7 @@ class MockAppConfig(Bunch):
self._remove_root = True
else:
self._remove_root = False
self.schema = self.MockSchema()
self.security = idencoding.IdEncodingHelper(id_secret='6e46ed6483a833c100e68cc3f1d0dd76')
self.database_connection = kwargs.get('database_connection', "sqlite:///:memory:")
self.use_remote_user = kwargs.get('use_remote_user', False)