mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' of github.com:anuprulez/galaxy into recommend_tools_run
This commit is contained in:
@@ -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"> 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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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', '.')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user