Merge remote-tracking branch 'upstream/dev' into v-slot-linting

This commit is contained in:
Dannon Baker
2020-03-10 16:42:35 -04:00
50 changed files with 1067 additions and 631 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">
@@ -22,6 +22,16 @@
</div>
</div>
</b-row>
<b-row class="ml-3 mb-1">
<i class="pref-icon pt-1 fa fa-lg fa-plus-square-o" />
<div class="pref-content pr-1">
<a @click="toggleNotifications" href="javascript:void(0)"><b>Enable notifications</b></a>
<div class="form-text text-muted">
Allow push and tab notifcations on job completion. To disable, revoke the site notification
privilege in your browser.
</div>
</div>
</b-row>
<p class="mt-2">
You are using <strong>{{ diskUsage }}</strong> of disk space in this Galaxy instance.
<span v-html="quotaUsageString"></span>
@@ -107,6 +117,18 @@ export default {
}
},
methods: {
toggleNotifications() {
Notification.requestPermission().then(function(permission) {
//If the user accepts, let's create a notification
if (permission === "granted") {
new Notification("Notifications enabled", {
icon: "static/favicon.ico"
});
} else {
alert("Notifications disabled, please re-enable through browser settings.");
}
});
},
openManageCustomBuilds() {
const Galaxy = getGalaxyInstance();
Galaxy.page.router.push(`${getAppRoot()}custom_builds`);
@@ -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>
@@ -13,22 +13,20 @@
<b-row>
<b-col>
<b-card id="data-table-card" flush>
<b-container v-slot:header>
<b-row align-v="center">
<b-col cols="auto">
<b-button
@click="reload()"
v-b-tooltip.hover
:title="'Reload ' + this.dataTable['name'] + ' tool data table'"
>
<span class="fa fa-refresh" />
</b-button>
</b-col>
<b-col>
<b>{{ this.dataTable["name"] }}</b>
</b-col>
</b-row>
</b-container>
<template v-slot:header>
<b-container>
<b-row align-v="center">
<b-col cols="auto">
<b-button @click="reload()" v-b-tooltip.hover :title="buttonLabel">
<span class="fa fa-refresh" />
</b-button>
</b-col>
<b-col>
<b>{{ dataTableName }}</b>
</b-col>
</b-row>
</b-container>
</template>
<b-table
:fields="fields(this.dataTable['columns'])"
:items="dataTable['data']"
@@ -68,6 +66,12 @@ export default {
};
},
computed: {
dataTableName() {
return this.dataTable && this.dataTable.name ? this.dataTable.name : "null";
},
buttonLabel() {
return `Reload ${this.dataTableName} tool data table`;
},
breadcrumbItems() {
return [
{
@@ -75,7 +79,7 @@ export default {
to: "/"
},
{
text: this.dataTable["name"]
text: this.dataTableName
}
];
}
@@ -88,7 +92,7 @@ export default {
},
reload() {
axios
.get(`${getAppRoot()}data_manager/reload_tool_data_tables?table_name=${this.dataTable["name"]}`)
.get(`${getAppRoot()}data_manager/reload_tool_data_tables?table_name=${this.dataTableName}`)
.then(response => {
if (response.data.dataTable) {
this.dataTable = response.data.dataTable;
-1
View File
@@ -267,7 +267,6 @@ const Collection = Backbone.Collection.extend({
cls: "loggedout-only",
tooltip: _l("Login"),
url: "login",
target: "galaxy_main",
noscratchbook: true
};
}
@@ -5,8 +5,10 @@ import { getAppRoot } from "onload/loadConfig";
import STATES from "mvc/dataset/states";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
import TAB_UPDATES from "mvc/user/tab-updates";
var logNamespace = "dataset";
var hiddenupdates = 1;
//==============================================================================
var searchableMixin = BASE_MVC.SearchableModelMixin;
/** @class base model for any DatasetAssociation (HDAs, LDDAs, DatasetCollectionDAs).
@@ -99,6 +101,24 @@ var DatasetAssociation = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend(
this.log(`${this} has changed state:`, currModel, newState);
if (this.inReadyState()) {
this.trigger("state:ready", currModel, newState, this.previous("state"));
if (newState != "discarded") {
if (newState === "ok") {
new Notification(`Job complete: ${this.get("name")}`, {
icon: "static/favicon.ico"
});
if (TAB_UPDATES.is_hidden()) {
TAB_UPDATES.hidden_count(hiddenupdates);
hiddenupdates++;
}
} else if (newState == "error") {
new Notification(`Job failure: ${this.get("name")}`, {
icon: "static/erricon.ico"
});
if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") {
TAB_UPDATES.change_favicon("static/erricon.ico");
}
}
}
}
});
// the download url (currently) relies on having a correct file extension
@@ -349,6 +369,14 @@ var DatasetAssociationCollection = Backbone.Collection.extend(BASE_MVC.LoggableM
}
);
window.addEventListener("focus", function() {
const testing = document.getElementById("tabicon");
if (testing && testing.href.search("erricon") != -1) {
TAB_UPDATES.change_favicon("static/favicon.ico");
}
document.title = "Galaxy";
hiddenupdates = 1;
});
//==============================================================================
export default {
DatasetAssociation: DatasetAssociation,
@@ -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() {
@@ -0,0 +1,51 @@
//Allow change tab on event=====================================================
function change_favicon(img) {
var favicon = document.createElement("link");
favicon.id = "tabicon";
favicon.setAttribute("rel", "shortcut icon");
var head = document.querySelector("head");
head.appendChild(favicon);
favicon.setAttribute("type", "image/png");
favicon.setAttribute("href", img);
}
//Check if browser is hidden-aware==============================================
function hidden_permit() {
var browsers = ["webkit", "moz", "ms", "o"];
if ("hidden" in document) {
return "hidden";
}
for (var i = 0; i < browsers.length; i++) {
if (browsers[i] + "Hidden" in document) {
return browsers[i] + "Hidden";
}
}
return null;
}
//Find out if browser tab is hidden=============================================
function is_hidden() {
var state = hidden_permit();
if (!state) {
return false;
}
return document[state];
}
//Generate new tab message to display on update in window=======================
function hidden_count(current) {
if (is_hidden()) {
if (current === 1) {
document.title = "Galaxy (1 job completed)";
} else {
document.title = "Galaxy (" + current + " jobs completed)";
}
}
}
export default {
change_favicon: change_favicon,
hidden_permit: hidden_permit,
is_hidden: is_hidden,
hidden_count: hidden_count
};
@@ -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;
+30 -15
View File
@@ -711,19 +711,6 @@
:Type: int
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``manage_dependency_relationships``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
Enable use of an in-memory registry with bi-directional
relationships between repositories (i.e., in addition to lists of
dependencies for a repository, keep an in-memory registry of
dependent items for each repository.
:Default: ``false``
:Type: bool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_data_table_config_path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1234,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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -3271,6 +3273,19 @@
:Type: bool
~~~~~~~~~~~~~~~~~~~~~~~~~~~
``job_metrics_config_file``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
XML config file that contains the job metric collection
configuration.
The value of this option will be resolved with respect to
<config_dir>.
:Default: ``job_metrics_conf.xml``
:Type: str
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``expose_potentially_sensitive_job_metrics``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -3673,7 +3688,7 @@
:Description:
CSS file to apply to "Galaxy Page" exports to PDF. Generally
prefer markdown_export_css, but this is here for deployments that
prefer markdown_export_css, but this is here for deployments that
would like to tailor different kinds of exports.
:Default: ``markdown_export_pages.css``
:Type: str
@@ -3685,7 +3700,7 @@
:Description:
CSS file to apply to invocation report exports to PDF. Generally
prefer markdown_export_css, but this is here for deployments that
prefer markdown_export_css, but this is here for deployments that
would like to tailor different kinds of exports.
:Default: ``markdown_export_invocation_reports.css``
:Type: str
+1 -1
View File
@@ -45,7 +45,7 @@ class AdminActions(object):
# If this is a default quota, create the DefaultQuotaAssociation
if params.default != 'no':
self.app.quota_agent.set_default_quota(params.default, quota)
message = "Default quota '%s' has been created."
message = "Default quota '%s' has been created." % quota.name
else:
# Create the UserQuotaAssociations
in_users = [self.sa_session.query(self.app.model.User).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_users)]
-2
View File
@@ -91,9 +91,7 @@ class UniverseApplication(config.ConfiguresGalaxyMixin):
check_migrate_tools = self.config.check_migrate_tools
self._configure_models(check_migrate_databases=self.config.check_migrate_databases, check_migrate_tools=check_migrate_tools, config_file=config_file)
# Manage installed tool shed repositories.
self.installed_repository_manager = InstalledRepositoryManager(self)
self._configure_datatypes_registry(self.installed_repository_manager)
galaxy.model.set_datatypes_registry(self.datatypes_registry)
+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,
)
@@ -561,8 +561,17 @@
<datatype extension="cel" type="galaxy.datatypes.microarrays:Cel" display_in_upload="true"/>
<datatype extension="gpr" type="galaxy.datatypes.microarrays:Gpr" display_in_upload="true"/>
<datatype extension="gal" type="galaxy.datatypes.microarrays:Gal" display_in_upload="true"/>
<datatype extension="rdata" type="galaxy.datatypes.binary:RData" display_in_upload="true" description="Stored data from an R session"/>
<datatype extension="rdata.sce" type="galaxy.datatypes.binary:RData" description="Stored RDS from a SingleCellObject" subclass="true" display_in_upload="true"/>
<datatype extension="rdata" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" display_in_upload="true" description="Stored data from an R session"/>
<datatype extension="rdata.sce" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true" description="Stored RDS from a SingleCellObject"/>
<datatype extension="rdata.xcms.raw" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.msnbase.raw" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.xcms.findchrompeaks" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.xcms.group" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.xcms.retcor" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.xcms.fillpeaks" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.camera.positive" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.camera.negative" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdata.camera.quick" type="galaxy.datatypes.binary:RData" mimetype="application/x-gzip" subclass="true" display_in_upload="true"/>
<datatype extension="rdock_as" type="galaxy.datatypes.binary:Binary" description="rDock active site format" subclass="true" display_in_upload="true"/>
<datatype extension="oxlicg" type="galaxy.datatypes.binary:OxliCountGraph" mimetype="application/octet-stream" display_in_upload="true"/>
<datatype extension="oxling" type="galaxy.datatypes.binary:OxliNodeGraph" mimetype="application/octet-stream" display_in_upload="true"/>
+15 -7
View File
@@ -451,12 +451,6 @@ galaxy:
# between 1 and 24.
#hours_between_check: 12
# Enable use of an in-memory registry with bi-directional
# relationships between repositories (i.e., in addition to lists of
# dependencies for a repository, keep an in-memory registry of
# dependent items for each repository.
#manage_dependency_relationships: false
# XML config file that contains data table entries for the
# ToolDataTableManager. This file is manually # maintained by the
# Galaxy administrator (.sample used if default does not exist).
@@ -693,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.
@@ -1611,6 +1613,12 @@ galaxy:
# see dataset paths.
#expose_dataset_path: false
# XML config file that contains the job metric collection
# configuration.
# 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).
#expose_potentially_sensitive_job_metrics: false
@@ -1815,7 +1823,7 @@ galaxy:
#markdown_export_css_pages: markdown_export_pages.css
# CSS file to apply to invocation report exports to PDF. Generally
# prefer markdown_export_css, but this is here for deployments that
# prefer markdown_export_css, but this is here for deployments that
# would like to tailor different kinds of exports.
#markdown_export_css_invocation_reports: markdown_export_invocation_reports.css
@@ -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
+10 -7
View File
@@ -61,8 +61,7 @@ def create_or_verify_database(url, galaxy_config_file, engine_options={}, app=No
# Apply all scripts to get to current version
migrate_to_current_version(engine, db_schema)
meta = MetaData(bind=engine)
if new_database:
def migrate_from_scratch():
log.info("Creating new database from scratch, skipping migrations")
current_version = migrate_repository.version().version
mapping.init(file_path='/tmp', url=url, map_install_models=map_install_models, create_tables=True)
@@ -70,6 +69,13 @@ def create_or_verify_database(url, galaxy_config_file, engine_options={}, app=No
db_schema = schema.ControlledSchema(engine, migrate_repository)
assert db_schema.version == current_version
migrate()
if app:
# skips the tool migration process.
app.new_installation = True
meta = MetaData(bind=engine)
if new_database:
migrate_from_scratch()
return
elif app and getattr(app.config, 'database_auto_migrate', False):
migrate()
@@ -79,12 +85,9 @@ def create_or_verify_database(url, galaxy_config_file, engine_options={}, app=No
try:
Table("dataset", meta, autoload=True)
except NoSuchTableError:
# No 'dataset' table means a completely uninitialized database. If we have an app, we'll
# set its new_installation setting to True so the tool migration process will be skipped.
if app:
app.new_installation = True
# No 'dataset' table means a completely uninitialized database.
log.info("No database, initializing")
migrate()
migrate_from_scratch()
return
try:
hda_table = Table("history_dataset_association", meta, autoload=True)
+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"):
+3 -1
View File
@@ -82,7 +82,7 @@ class ObjectStore(object):
000/obj.id)
"""
def __init__(self, config, config_dict={}, **kwargs):
def __init__(self, config, config_dict=None, **kwargs):
"""
:type config: object
:param config: An object, most likely populated from
@@ -95,6 +95,8 @@ class ObjectStore(object):
parent directory those directories will be created.
* new_file_path -- Used to set the 'temp' extra_dir.
"""
if config_dict is None:
config_dict = {}
self.running = True
self.config = config
self.check_old_style = config.object_store_check_old_style
+37 -29
View File
@@ -125,7 +125,8 @@ class CloudConfigMixin(object):
'cache': {
'size': self.cache_size,
'path': self.staging_path,
}
},
'enable_cache_monitor': False,
}
@@ -138,7 +139,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
store_type = 's3'
def __init__(self, config, config_dict):
super(S3ObjectStore, self).__init__(config)
super(S3ObjectStore, self).__init__(config, config_dict)
self.transfer_progress = 0
@@ -146,6 +147,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
bucket_dict = config_dict['bucket']
connection_dict = config_dict.get('connection', {})
cache_dict = config_dict['cache']
self.enable_cache_monitor = config_dict.get('enable_cache_monitor', True)
self.access_key = auth_dict.get('access_key')
self.secret_key = auth_dict.get('secret_key')
@@ -167,9 +169,6 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
(e['type'], e['path']) for e in config_dict.get('extra_dirs', []))
self.extra_dirs.update(extra_dirs)
log.debug("Object cache dir: %s", self.staging_path)
log.debug(" job work dir: %s", self.extra_dirs['job_work'])
self._initialize()
def _initialize(self):
@@ -187,9 +186,17 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
'conn_path': self.conn_path}
self._configure_connection()
self.bucket = self._get_bucket(self.bucket)
self._bucket = self._get_bucket(self.bucket)
self.start_cache_monitor()
# Test if 'axel' is available for parallel download and pull the key into cache
if which('axel'):
self.use_axel = True
else:
self.use_axel = False
def start_cache_monitor(self):
# Clean cache only if value is set in galaxy.ini
if self.cache_size != -1:
if self.cache_size != -1 and self.enable_cache_monitor:
# Convert GBs to bytes for comparison
self.cache_size = self.cache_size * 1073741824
# Helper for interruptable sleep
@@ -197,11 +204,6 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
self.cache_monitor_thread = threading.Thread(target=self.__cache_monitor)
self.cache_monitor_thread.start()
log.info("Cache cleaner manager started")
# Test if 'axel' is available for parallel download and pull the key into cache
if which('axel'):
self.use_axel = True
else:
self.use_axel = False
def _configure_connection(self):
log.debug("Configuring S3 Connection")
@@ -325,7 +327,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
# alt_name can contain parent directory references, but S3 will not
# follow them, so if they are valid we normalize them out
alt_name = os.path.normpath(alt_name)
rel_path = os.path.join(*directory_hash_id(obj.id))
rel_path = os.path.join(*directory_hash_id(self._get_object_id(obj)))
if extra_dir is not None:
if extra_dir_at_root:
rel_path = os.path.join(extra_dir, rel_path)
@@ -334,7 +336,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
# for JOB_WORK directory
if obj_dir:
rel_path = os.path.join(rel_path, str(obj.id))
rel_path = os.path.join(rel_path, str(self._get_object_id(obj)))
if base_dir:
base = self.extra_dirs.get(base_dir)
return os.path.join(base, rel_path)
@@ -343,7 +345,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
rel_path = '%s/' % rel_path
if not dir_only:
rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % obj.id)
rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % self._get_object_id(obj))
return rel_path
def _get_cache_path(self, rel_path):
@@ -354,7 +356,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
def _get_size_in_s3(self, rel_path):
try:
key = self.bucket.get_key(rel_path)
key = self._bucket.get_key(rel_path)
if key:
return key.size
except S3ResponseError:
@@ -367,19 +369,17 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
# A hackish way of testing if the rel_path is a folder vs a file
is_dir = rel_path[-1] == '/'
if is_dir:
keyresult = self.bucket.get_all_keys(prefix=rel_path)
keyresult = self._bucket.get_all_keys(prefix=rel_path)
if len(keyresult) > 0:
exists = True
else:
exists = False
else:
key = Key(self.bucket, rel_path)
key = Key(self._bucket, rel_path)
exists = key.exists()
except S3ResponseError:
log.exception("Trouble checking existence of S3 key '%s'", rel_path)
return False
if rel_path[0] == '/':
raise
return exists
def _in_cache(self, rel_path):
@@ -427,7 +427,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
def _download(self, rel_path):
try:
log.debug("Pulling key '%s' into cache to %s", rel_path, self._get_cache_path(rel_path))
key = self.bucket.get_key(rel_path)
key = self._bucket.get_key(rel_path)
# Test if cache is large enough to hold the new file
if self.cache_size > 0 and key.size > self.cache_size:
log.critical("File %s is larger (%s) than the cache size (%s). Cannot download.",
@@ -446,7 +446,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
key.get_contents_to_filename(self._get_cache_path(rel_path), cb=self._transfer_cb, num_cb=10)
return True
except S3ResponseError:
log.exception("Problem downloading key '%s' from S3 bucket '%s'", rel_path, self.bucket.name)
log.exception("Problem downloading key '%s' from S3 bucket '%s'", rel_path, self._bucket.name)
return False
def _push_to_os(self, rel_path, source_file=None, from_string=None):
@@ -460,7 +460,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
try:
source_file = source_file if source_file else self._get_cache_path(rel_path)
if os.path.exists(source_file):
key = Key(self.bucket, rel_path)
key = Key(self._bucket, rel_path)
if os.path.getsize(source_file) == 0 and key.exists():
log.debug("Wanted to push file '%s' to S3 key '%s' but its size is 0; skipping.", source_file, rel_path)
return True
@@ -478,7 +478,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
cb=self._transfer_cb,
num_cb=10)
else:
multipart_upload(self.s3server, self.bucket, key.name, source_file, mb_size)
multipart_upload(self.s3server, self._bucket, key.name, source_file, mb_size)
end_time = datetime.now()
log.debug("Pushed cache file '%s' to key '%s' (%s bytes transfered in %s sec)",
source_file, rel_path, os.path.getsize(source_file), end_time - start_time)
@@ -547,7 +547,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
alt_name = kwargs.get('alt_name', None)
# Construct hashed path
rel_path = os.path.join(*directory_hash_id(obj.id))
rel_path = os.path.join(*directory_hash_id(self._get_object_id(obj)))
# Optionally append extra_dir
if extra_dir is not None:
@@ -568,7 +568,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
# self._push_to_os(s3_dir, from_string='')
# If instructed, create the dataset in cache & in S3
if not dir_only:
rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % obj.id)
rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % self._get_object_id(obj))
open(os.path.join(self.staging_path, rel_path), 'w').close()
self._push_to_os(rel_path, from_string='')
@@ -609,7 +609,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
# but requires iterating through each individual key in S3 and deleing it.
if entire_dir and extra_dir:
shutil.rmtree(self._get_cache_path(rel_path))
results = self.bucket.get_all_keys(prefix=rel_path)
results = self._bucket.get_all_keys(prefix=rel_path)
for key in results:
log.debug("Deleting key %s", key.name)
key.delete()
@@ -619,7 +619,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
os.unlink(self._get_cache_path(rel_path))
# Delete from S3 as well
if self._key_exists(rel_path):
key = Key(self.bucket, rel_path)
key = Key(self._bucket, rel_path)
log.debug("Deleting key %s", key.name)
key.delete()
return True
@@ -707,7 +707,7 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
if self.exists(obj, **kwargs):
rel_path = self._construct_path(obj, **kwargs)
try:
key = Key(self.bucket, rel_path)
key = Key(self._bucket, rel_path)
return key.generate_url(expires_in=86400) # 24hrs
except S3ResponseError:
log.exception("Trouble generating URL for dataset '%s'", rel_path)
@@ -716,6 +716,14 @@ class S3ObjectStore(ConcreteObjectStore, CloudConfigMixin):
def get_store_usage_percent(self):
return 0.0
def shutdown(self):
self.running = False
thread = getattr(self, 'cache_monitor_thread', None)
if thread:
log.debug("Shutting down thread")
self.sleeper.wake()
thread.join(5)
class SwiftObjectStore(S3ObjectStore):
"""
@@ -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]
+17 -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:
@@ -294,7 +310,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'
@@ -215,10 +215,6 @@ class InstallToolDependencyManager(object):
if tool_dependency and tool_dependency.status in [self.install_model.ToolDependency.installation_status.INSTALLED,
self.install_model.ToolDependency.installation_status.ERROR]:
installed_packages.append(tool_dependency)
if self.app.config.manage_dependency_relationships:
# Add the tool_dependency to the in-memory dictionaries in the installed_repository_manager.
self.app.installed_repository_manager.handle_tool_dependency_install(tool_shed_repository,
tool_dependency)
return installed_packages
def install_via_fabric(self, tool_shed_repository, tool_dependency, install_dir, package_name=None, custom_fabfile_path=None,
@@ -946,10 +942,6 @@ class InstallRepositoryManager(object):
basic_util.remove_dir(work_dir)
self.update_tool_shed_repository_status(tool_shed_repository,
self.install_model.ToolShedRepository.installation_status.INSTALLED)
if self.app.config.manage_dependency_relationships:
# Add the installed repository and any tool dependencies to the in-memory dictionaries
# in the installed_repository_manager.
self.app.installed_repository_manager.handle_repository_install(tool_shed_repository)
else:
# An error occurred while cloning the repository, so reset everything necessary to enable another attempt.
repository_util.set_repository_attributes(self.app,
@@ -79,9 +79,6 @@ class InstalledRepositoryManager(object):
# whose values are a list of tuples defining tool_dependency objects (whose status is 'Installed') that require the key
# at runtime. The value defines the entire tool dependency tree.
self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = {}
if app.config.manage_dependency_relationships:
# Load defined dependency relationships for installed tool shed repositories and their contents.
self.load_dependency_relationships()
def activate_repository(self, repository):
"""Activate an installed tool shed repository that has been marked as deactivated."""
@@ -651,90 +648,6 @@ class InstalledRepositoryManager(object):
deleted_tool_dependency_names.append(original_dependency_val_dict['name'])
return updated_tool_dependency_names, deleted_tool_dependency_names
def handle_repository_install(self, repository):
"""Load the dependency relationships for a repository that was just installed or reinstalled."""
# Populate self.repository_dependencies_of_installed_repositories.
self.add_entry_to_repository_dependencies_of_installed_repositories(repository)
# Populate self.installed_repository_dependencies_of_installed_repositories.
self.add_entry_to_installed_repository_dependencies_of_installed_repositories(repository)
# Populate self.tool_dependencies_of_installed_repositories.
self.add_entry_to_tool_dependencies_of_installed_repositories(repository)
# Populate self.installed_tool_dependencies_of_installed_repositories.
self.add_entry_to_installed_tool_dependencies_of_installed_repositories(repository)
for tool_dependency in repository.tool_dependencies:
# Populate self.runtime_tool_dependencies_of_installed_tool_dependencies.
self.add_entry_to_runtime_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
# Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.
self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
def handle_repository_uninstall(self, repository):
"""Remove the dependency relationships for a repository that was just uninstalled."""
for tool_dependency in repository.tool_dependencies:
tool_dependency_tup = self.get_tool_dependency_tuple_for_installed_repository_manager(tool_dependency)
# Remove this tool_dependency from all values in
# self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies
altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = {}
for (td_tup, installed_runtime_dependent_tool_dependency_tups) in self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.items():
if tool_dependency_tup in installed_runtime_dependent_tool_dependency_tups:
# Remove the tool_dependency from the list.
installed_runtime_dependent_tool_dependency_tups.remove(tool_dependency_tup)
# Add the possibly altered list to the altered dictionary.
altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies[td_tup] = \
installed_runtime_dependent_tool_dependency_tups
self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = \
altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies
# Remove the entry for this tool_dependency from self.runtime_tool_dependencies_of_installed_tool_dependencies.
self.remove_entry_from_runtime_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
# Remove the entry for this tool_dependency from
# self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.
self.remove_entry_from_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
# Remove this repository's entry from self.installed_tool_dependencies_of_installed_repositories.
self.remove_entry_from_installed_tool_dependencies_of_installed_repositories(repository)
# Remove this repository's entry from self.tool_dependencies_of_installed_repositories
self.remove_entry_from_tool_dependencies_of_installed_repositories(repository)
# Remove this repository's entry from self.installed_repository_dependencies_of_installed_repositories.
self.remove_entry_from_installed_repository_dependencies_of_installed_repositories(repository)
# Remove this repository's entry from self.repository_dependencies_of_installed_repositories.
self.remove_entry_from_repository_dependencies_of_installed_repositories(repository)
def handle_tool_dependency_install(self, repository, tool_dependency):
"""Load the dependency relationships for a tool dependency that was just installed independently of its containing repository."""
# The received repository must have a status of 'Installed'. The value of tool_dependency.status will either be
# 'Installed' or 'Error', but we only need to change the in-memory dictionaries if it is 'Installed'.
if tool_dependency.is_installed:
# Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.
self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
# Populate self.installed_tool_dependencies_of_installed_repositories.
repository_tup = self.get_repository_tuple_for_installed_repository_manager(repository)
tool_dependency_tup = self.get_tool_dependency_tuple_for_installed_repository_manager(tool_dependency)
if repository_tup in self.installed_tool_dependencies_of_installed_repositories:
self.installed_tool_dependencies_of_installed_repositories[repository_tup].append(tool_dependency_tup)
else:
self.installed_tool_dependencies_of_installed_repositories[repository_tup] = [tool_dependency_tup]
def load_dependency_relationships(self):
"""Load relationships for all installed repositories and tool dependencies into in-memnory dictionaries."""
# Get the list of installed tool shed repositories.
for repository in self.context.query(self.app.install_model.ToolShedRepository) \
.filter(self.app.install_model.ToolShedRepository.table.c.status ==
self.app.install_model.ToolShedRepository.installation_status.INSTALLED):
# Populate self.repository_dependencies_of_installed_repositories.
self.add_entry_to_repository_dependencies_of_installed_repositories(repository)
# Populate self.installed_repository_dependencies_of_installed_repositories.
self.add_entry_to_installed_repository_dependencies_of_installed_repositories(repository)
# Populate self.tool_dependencies_of_installed_repositories.
self.add_entry_to_tool_dependencies_of_installed_repositories(repository)
# Populate self.installed_tool_dependencies_of_installed_repositories.
self.add_entry_to_installed_tool_dependencies_of_installed_repositories(repository)
# Get the list of installed tool dependencies.
for tool_dependency in self.context.query(self.app.install_model.ToolDependency) \
.filter(self.app.install_model.ToolDependency.table.c.status ==
self.app.install_model.ToolDependency.installation_status.INSTALLED):
# Populate self.runtime_tool_dependencies_of_installed_tool_dependencies.
self.add_entry_to_runtime_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
# Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.
self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies(tool_dependency)
def load_proprietary_datatypes(self):
cdl = custom_datatype_manager.CustomDatatypeLoader(self.app)
for tool_shed_repository in self.context.query(self.install_model.ToolShedRepository) \
@@ -814,10 +727,6 @@ class InstalledRepositoryManager(object):
if remove_from_disk:
repository.status = self.app.install_model.ToolShedRepository.installation_status.UNINSTALLED
repository.error_message = None
if self.app.config.manage_dependency_relationships:
# Remove the uninstalled repository and any tool dependencies from the in-memory dictionaries in the
# installed_repository_manager.
self.handle_repository_uninstall(repository)
else:
repository.status = self.app.install_model.ToolShedRepository.installation_status.DEACTIVATED
self.app.install_model.context.current.add(repository)
+1 -1
View File
@@ -1913,7 +1913,7 @@ class DataToolParameter(BaseDataToolParameter):
ref = getattr(ref, attribute)
if call_attribute:
ref = ref()
return ref
return str(ref)
def to_dict(self, trans, other_values={}):
# create dictionary and fill default parameters
+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
+23 -11
View File
@@ -537,15 +537,6 @@ mapping:
server process should be able to check for repository updates. The
setting for hours_between_check should be an integer between 1 and 24.
manage_dependency_relationships:
type: bool
default: false
required: false
desc: |
Enable use of an in-memory registry with bi-directional relationships between
repositories (i.e., in addition to lists of dependencies for a repository,
keep an in-memory registry of dependent items for each repository.
tool_data_table_config_path:
type: str
default: config/tool_data_table_conf.xml
@@ -911,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: >-
@@ -2417,6 +2420,15 @@ mapping:
Details" option in the history. This option also exposes the command line to
non-administrative users. Administrators can always see dataset paths.
job_metrics_config_file:
type: str
default: job_metrics_conf.xml
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:
type: bool
default: false
@@ -2728,7 +2740,7 @@ mapping:
default: markdown_export_pages.css
required: false
desc: |
CSS file to apply to "Galaxy Page" exports to PDF. Generally prefer
CSS file to apply to "Galaxy Page" exports to PDF. Generally prefer
markdown_export_css, but this is here for deployments that
would like to tailor different kinds of exports.
@@ -2737,7 +2749,7 @@ mapping:
default: markdown_export_invocation_reports.css
required: false
desc: |
CSS file to apply to invocation report exports to PDF. Generally prefer
CSS file to apply to invocation report exports to PDF. Generally prefer
markdown_export_css, but this is here for deployments that
would like to tailor different kinds of exports.
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,113 @@
import os
import string
import subprocess
from galaxy_test.driver import integration_util
OBJECT_STORE_HOST = os.environ.get('GALAXY_INTEGRATION_OBJECT_STORE_HOST', '127.0.0.1')
OBJECT_STORE_PORT = int(os.environ.get('GALAXY_INTEGRATION_OBJECT_STORE_PORT', 9000))
OBJECT_STORE_ACCESS_KEY = os.environ.get('GALAXY_INTEGRATION_OBJECT_STORE_ACCESS_KEY', 'minioadmin')
OBJECT_STORE_SECRET_KEY = os.environ.get('GALAXY_INTEGRATION_OBJECT_STORE_SECRET_KEY', 'minioadmin')
OBJECT_STORE_CONFIG = string.Template("""
<object_store type="hierarchical" id="primary">
<backends>
<object_store id="swifty" type="swift" weight="1" order="0">
<auth access_key="${access_key}" secret_key="${secret_key}" />
<bucket name="galaxy" use_reduced_redundancy="False" max_chunk_size="250"/>
<connection host="${host}" port="${port}" is_secure="False" conn_path="" multipart="True"/>
<cache path="${temp_directory}/object_store_cache" size="1000" />
<extra_dir type="job_work" path="${temp_directory}/job_working_directory_swift"/>
<extra_dir type="temp" path="${temp_directory}/tmp_swift"/>
</object_store>
</backends>
</object_store>
""")
TEST_TOOL_IDS = [
"multi_output",
"multi_output_configured",
"multi_output_assign_primary",
"multi_output_recurse",
"tool_provided_metadata_1",
"tool_provided_metadata_2",
"tool_provided_metadata_3",
"tool_provided_metadata_4",
"tool_provided_metadata_5",
"tool_provided_metadata_6",
"tool_provided_metadata_7",
"tool_provided_metadata_8",
"tool_provided_metadata_9",
"tool_provided_metadata_10",
"tool_provided_metadata_11",
"tool_provided_metadata_12",
"composite_output",
"composite_output_tests",
"metadata",
"metadata_bam",
"output_format",
"output_auto_format",
]
def start_minio(container_name):
minio_start_args = [
'docker',
'run',
'-p',
'{port}:9000'.format(port=OBJECT_STORE_PORT),
'-d',
'--name',
container_name,
# '--rm',
'minio/minio:latest',
'server',
'/tmp/data']
subprocess.check_call(minio_start_args)
def stop_minio(container_name):
subprocess.check_call(['docker', 'rm', '-f', container_name])
@integration_util.skip_unless_docker()
class SwiftObjectStoreIntegrationTestCase(integration_util.IntegrationTestCase):
@classmethod
def setUpClass(cls):
cls.container_name = "%s_container" % cls.__name__
start_minio(cls.container_name)
super(SwiftObjectStoreIntegrationTestCase, cls).setUpClass()
@classmethod
def tearDownClass(cls):
stop_minio(cls.container_name)
super(SwiftObjectStoreIntegrationTestCase, cls).tearDownClass()
@classmethod
def handle_galaxy_config_kwds(cls, config):
temp_directory = cls._test_driver.mkdtemp()
cls.object_stores_parent = temp_directory
config_path = os.path.join(temp_directory, "object_store_conf.xml")
config["object_store_store_by"] = "uuid"
# This doesn't quite work yet, fails with extra_files_path
# config["metadata_strategy"] = "extended"
config["outpus_to_working_dir"] = True
config["retry_metadata_internally"] = False
with open(config_path, "w") as f:
f.write(
OBJECT_STORE_CONFIG.safe_substitute(
{
"temp_directory": temp_directory,
"host": OBJECT_STORE_HOST,
"port": OBJECT_STORE_PORT,
"access_key": OBJECT_STORE_ACCESS_KEY,
"secret_key": OBJECT_STORE_SECRET_KEY,
}
)
)
config["object_store_config_file"] = config_path
instance = integration_util.integration_module_instance(SwiftObjectStoreIntegrationTestCase)
test_tools = integration_util.integration_tool_runner(TEST_TOOL_IDS)
+2 -1
View File
@@ -178,6 +178,7 @@ DO_NOT_TEST = [
'id_secret', # broken: default overridden
'job_config', # no obvious testable defaults
'job_config_file', # broken: remove 'config/' prefix from schema
'job_metrics_config_file',
'job_working_directory', # broken; may or may not be able to test
'library_import_dir', # broken: default overridden
'logging', # mapping loaded in config/
@@ -253,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)