mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #12327 from jmchilton/tool_panel_views_0
Implement tool panel views.
This commit is contained in:
+2
-1
@@ -87,7 +87,8 @@
|
||||
"vueisotope": "^3.1.2",
|
||||
"vuex": "^3.4.0",
|
||||
"vuex-cache": "^3.2.0",
|
||||
"vuex-persist": "^3.1.3"
|
||||
"vuex-persist": "^3.1.3",
|
||||
"vuex-persistedstate": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "gulp && yarn run save-build-hash && yarn run webpack-watch",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<b-dropdown
|
||||
no-caret
|
||||
right
|
||||
role="button"
|
||||
title="Panel Views"
|
||||
variant="link"
|
||||
aria-label="View all tool panel configurations"
|
||||
class="tool-panel-dropdown float-right"
|
||||
size="sm"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<span class="fa fa-list" />
|
||||
<!-- Maybe cog? -->
|
||||
</template>
|
||||
<PanelViewMenuItem
|
||||
:current-panel-view="currentPanelView"
|
||||
:panel-view="defaultPanelView"
|
||||
@onSelect="updatePanelView"
|
||||
/>
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
<b-dropdown-group v-for="group in groupedPanelViews" :key="group.type" :id="group.type" :header="group.title">
|
||||
<PanelViewMenuItem
|
||||
v-for="(panelView, key) in group.panelViews"
|
||||
:key="key"
|
||||
:current-panel-view="currentPanelView"
|
||||
:panel-view="panelView"
|
||||
@onSelect="updatePanelView"
|
||||
/>
|
||||
</b-dropdown-group>
|
||||
<b-dropdown-divider v-if="groupedPanelViews.length > 0"></b-dropdown-divider>
|
||||
<PanelViewMenuItem
|
||||
v-for="(panelView, key) in ungroupedPanelViews"
|
||||
:key="key"
|
||||
:current-panel-view="currentPanelView"
|
||||
:panel-view="panelView"
|
||||
@onSelect="updatePanelView"
|
||||
/>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PanelViewMenuItem from "./PanelViewMenuItem";
|
||||
|
||||
const groupsDefinitions = [
|
||||
{ type: "ontology", title: "... by Ontology" },
|
||||
{ type: "activity", title: "... for Activity" },
|
||||
{ type: "publication", title: "... from Publication" },
|
||||
{ type: "training", title: "... for Training" },
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { PanelViewMenuItem },
|
||||
props: {
|
||||
panelViews: {
|
||||
type: Object,
|
||||
},
|
||||
currentPanelView: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
defaultPanelView() {
|
||||
return this.panelViewsOfType("default")[0];
|
||||
},
|
||||
groupedPanelViews() {
|
||||
const groups = [];
|
||||
for (const group of groupsDefinitions) {
|
||||
const viewType = group.type;
|
||||
const panelViews = this.panelViewsOfType(viewType);
|
||||
if (panelViews.length > 0) {
|
||||
groups.push({
|
||||
type: viewType,
|
||||
title: group.title,
|
||||
panelViews: panelViews,
|
||||
});
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
ungroupedPanelViews() {
|
||||
return this.panelViewsOfType("generic");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePanelView(panelView) {
|
||||
this.$emit("updatePanelView", panelView.id);
|
||||
},
|
||||
panelViewsOfType(panelViewType) {
|
||||
const panelViews = [];
|
||||
for (const panelViewId in this.panelViews) {
|
||||
const panelView = this.panelViews[panelViewId];
|
||||
if (panelView.view_type == panelViewType) {
|
||||
panelViews.push(panelView);
|
||||
}
|
||||
}
|
||||
return panelViews;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<b-dropdown-item
|
||||
:active="currentPanelView == panelView.id"
|
||||
:disabled="currentPanelView == panelView.id"
|
||||
@click="onClick"
|
||||
>
|
||||
<span :class="icon" />
|
||||
<span :title="title" v-localize>{{ name }}</span>
|
||||
</b-dropdown-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const types_to_icons = {
|
||||
default: "fa-backspace",
|
||||
generic: "fa-filter",
|
||||
ontology: "fa-sitemap",
|
||||
activity: "fa-project-diagram",
|
||||
publication: "fa-newspaper",
|
||||
training: "fa-graduation-cap",
|
||||
};
|
||||
|
||||
export default {
|
||||
props: {
|
||||
currentPanelView: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
panelView: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.panelView.description;
|
||||
},
|
||||
icon() {
|
||||
const viewType = this.panelView.view_type;
|
||||
const faIcon = types_to_icons[viewType];
|
||||
return ["fa", faIcon];
|
||||
},
|
||||
name() {
|
||||
return this.panelView.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit("onSelect", this.panelView);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -14,6 +14,10 @@ export default {
|
||||
DelayedInput,
|
||||
},
|
||||
props: {
|
||||
currentPanelView: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -46,7 +50,7 @@ export default {
|
||||
this.loading = true;
|
||||
axios
|
||||
.get(`${getAppRoot()}api/tools`, {
|
||||
params: { q },
|
||||
params: { q, view: this.currentPanelView },
|
||||
})
|
||||
.then((response) => {
|
||||
this.loading = false;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<ConfigProvider class="d-flex flex-column" v-slot="{ config }">
|
||||
<ToolPanelViewProvider
|
||||
v-slot="{ currentPanel, currentPanelView }"
|
||||
:site-default-panel-view="config.default_panel_view"
|
||||
v-if="config.default_panel_view"
|
||||
>
|
||||
<ToolBox
|
||||
:toolbox="currentPanel"
|
||||
:panel-views="config.panel_views"
|
||||
:current-panel-view="currentPanelView"
|
||||
:stored-workflow-menu-entries="storedWorkflowMenuEntries"
|
||||
:workflow-title="workflowTitle"
|
||||
@updatePanelView="updatePanelView"
|
||||
v-if="currentPanelView"
|
||||
>
|
||||
</ToolBox>
|
||||
</ToolPanelViewProvider>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ToolBox from "./ToolBox";
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import ToolPanelViewProvider from "components/providers/ToolPanelViewProvider";
|
||||
import _l from "utils/localization";
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfigProvider,
|
||||
ToolBox,
|
||||
ToolPanelViewProvider,
|
||||
},
|
||||
props: {
|
||||
storedWorkflowMenuEntries: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
workflowTitle: {
|
||||
type: String,
|
||||
default: _l("Workflows"),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePanelView(panelView) {
|
||||
this.setCurrentPanelView(panelView);
|
||||
},
|
||||
...mapActions("panels", ["setCurrentPanelView"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<ConfigProvider class="d-flex flex-column" v-slot="{ config }">
|
||||
<ToolPanelViewProvider
|
||||
v-slot="{ currentPanel, currentPanelView }"
|
||||
:site-default-panel-view="config.default_panel_view"
|
||||
v-if="config.default_panel_view"
|
||||
>
|
||||
<ToolBoxWorkflow
|
||||
:toolbox="currentPanel"
|
||||
:panel-views="config.panel_views"
|
||||
:current-panel-view="currentPanelView"
|
||||
:workflows="workflows"
|
||||
:data-managers="dataManagers"
|
||||
:module-sections="moduleSections"
|
||||
@updatePanelView="updatePanelView"
|
||||
@onInsertTool="onInsertTool"
|
||||
@onInsertModule="onInsertModule"
|
||||
@onInsertWorkflow="onInsertWorkflow"
|
||||
@onInsertWorkflowSteps="onInsertWorkflowSteps"
|
||||
v-if="currentPanelView"
|
||||
>
|
||||
</ToolBoxWorkflow>
|
||||
</ToolPanelViewProvider>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ToolBoxWorkflow from "./ToolBoxWorkflow";
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import ToolPanelViewProvider from "components/providers/ToolPanelViewProvider";
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfigProvider,
|
||||
ToolBoxWorkflow,
|
||||
ToolPanelViewProvider,
|
||||
},
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dataManagers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
moduleSections: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePanelView(panelView) {
|
||||
this.setCurrentPanelView(panelView);
|
||||
},
|
||||
...mapActions("panels", ["setCurrentPanelView"]),
|
||||
onInsertTool(toolId, toolName) {
|
||||
this.$emit("onInsertTool", toolId, toolName);
|
||||
},
|
||||
onInsertModule(moduleName, moduleTitle) {
|
||||
this.$emit("onInsertModule", moduleName, moduleTitle);
|
||||
},
|
||||
onInsertWorkflow(workflowId, workflowName) {
|
||||
this.$emit("onInsertWorkflow", workflowId, workflowName);
|
||||
},
|
||||
onInsertWorkflowSteps(workflowId, workflowStepCount) {
|
||||
this.$emit("onInsertWorkflowSteps", workflowId, workflowStepCount);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -4,12 +4,24 @@
|
||||
<div class="unified-panel-header-inner">
|
||||
<div class="panel-header-buttons">
|
||||
<favorites-button :query="query" @onFavorites="onFavorites" v-if="isUser" />
|
||||
<panel-view-button
|
||||
:panel-views="panelViews"
|
||||
:current-panel-view="currentPanelView"
|
||||
@updatePanelView="updatePanelView"
|
||||
v-if="panelViews && Object.keys(panelViews).length > 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-header-text">Tools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-controls">
|
||||
<tool-search :query="query" placeholder="search tools" @onQuery="onQuery" @onResults="onResults" />
|
||||
<tool-search
|
||||
:current-panel-view="currentPanelView"
|
||||
:query="query"
|
||||
placeholder="search tools"
|
||||
@onQuery="onQuery"
|
||||
@onResults="onResults"
|
||||
/>
|
||||
<upload-button />
|
||||
<div class="py-2" v-if="hasResults">
|
||||
<b-button @click="onToggle" size="sm" class="w-100">
|
||||
@@ -52,16 +64,17 @@ import ToolSection from "./Common/ToolSection";
|
||||
import ToolSearch from "./Common/ToolSearch";
|
||||
import { UploadButton, openGlobalUploadModal } from "components/Upload";
|
||||
import FavoritesButton from "./Buttons/FavoritesButton";
|
||||
import PanelViewButton from "./Buttons/PanelViewButton";
|
||||
import { filterToolSections, filterTools } from "./utilities";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import _l from "utils/localization";
|
||||
|
||||
export default {
|
||||
name: "ToolBox",
|
||||
components: {
|
||||
UploadButton,
|
||||
FavoritesButton,
|
||||
PanelViewButton,
|
||||
ToolSection,
|
||||
ToolSearch,
|
||||
},
|
||||
@@ -81,6 +94,12 @@ export default {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
panelViews: {
|
||||
type: Object,
|
||||
},
|
||||
currentPanelView: {
|
||||
type: String,
|
||||
},
|
||||
storedWorkflowMenuEntries: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -163,6 +182,9 @@ export default {
|
||||
this.buttonText = this.showSections ? "Hide Sections" : "Show Sections";
|
||||
this.buttonIcon = this.showSections ? "fa fa-eye-slash" : "fa fa-eye";
|
||||
},
|
||||
updatePanelView(panelView) {
|
||||
this.$emit("updatePanelView", panelView);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,24 @@
|
||||
<div class="unified-panel">
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<div class="panel-header-buttons">
|
||||
<panel-view-button
|
||||
:panel-views="panelViews"
|
||||
:current-panel-view="currentPanelView"
|
||||
@updatePanelView="updatePanelView"
|
||||
v-if="panelViews && Object.keys(panelViews).length > 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-header-text">Tools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-controls">
|
||||
<tool-search placeholder="search tools" @onQuery="onQuery" @onResults="onResults" />
|
||||
<tool-search
|
||||
:current-panel-view="currentPanelView"
|
||||
placeholder="search tools"
|
||||
@onQuery="onQuery"
|
||||
@onResults="onResults"
|
||||
/>
|
||||
</div>
|
||||
<div class="unified-panel-body">
|
||||
<div class="toolMenuContainer">
|
||||
@@ -58,12 +71,14 @@ import _l from "utils/localization";
|
||||
import ToolSection from "./Common/ToolSection";
|
||||
import ToolSearch from "./Common/ToolSearch";
|
||||
import { filterToolSections } from "./utilities";
|
||||
import PanelViewButton from "./Buttons/PanelViewButton";
|
||||
|
||||
export default {
|
||||
name: "ToolBox",
|
||||
components: {
|
||||
ToolSection,
|
||||
ToolSearch,
|
||||
PanelViewButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -76,6 +91,12 @@ export default {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
panelViews: {
|
||||
type: Object,
|
||||
},
|
||||
currentPanelView: {
|
||||
type: String,
|
||||
},
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -147,6 +168,9 @@ export default {
|
||||
onInsertWorkflowSteps(workflow) {
|
||||
this.$emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
|
||||
},
|
||||
updatePanelView(panelView) {
|
||||
this.$emit("updatePanelView", panelView);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
<div v-show="isCanvas">
|
||||
<SidePanel id="left" side="left">
|
||||
<template v-slot:panel>
|
||||
<ToolBoxWorkflow
|
||||
:toolbox="toolbox"
|
||||
<ProviderAwareToolBoxWorkflow
|
||||
:module-sections="moduleSections"
|
||||
:data-managers="dataManagers"
|
||||
:workflows="workflows"
|
||||
@@ -186,7 +185,7 @@ import WorkflowOptions from "./Options";
|
||||
import FormDefault from "./Forms/FormDefault";
|
||||
import FormTool from "./Forms/FormTool";
|
||||
import MarkdownEditor from "components/Markdown/MarkdownEditor";
|
||||
import ToolBoxWorkflow from "components/Panels/ToolBoxWorkflow";
|
||||
import ProviderAwareToolBoxWorkflow from "components/Panels/ProviderAwareToolBoxWorkflow";
|
||||
import SidePanel from "components/Panels/SidePanel";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import reportDefault from "./reportDefault";
|
||||
@@ -205,7 +204,7 @@ export default {
|
||||
MarkdownEditor,
|
||||
SidePanel,
|
||||
StateUpgradeModal,
|
||||
ToolBoxWorkflow,
|
||||
ProviderAwareToolBoxWorkflow,
|
||||
FormDefault,
|
||||
FormTool,
|
||||
WorkflowOptions,
|
||||
@@ -241,10 +240,6 @@ export default {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
toolbox: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
siteDefaultPanelView: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("panels", ["currentPanel", "currentPanelView"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions("panels", ["initCurrentPanelView"]),
|
||||
},
|
||||
created() {
|
||||
this.initCurrentPanelView(this.siteDefaultPanelView);
|
||||
},
|
||||
render() {
|
||||
return this.$scopedSlots.default({
|
||||
currentPanel: this.currentPanel,
|
||||
currentPanelView: this.currentPanelView,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import Backbone from "backbone";
|
||||
import _l from "utils/localization";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import ToolBox from "../../components/Panels/ToolBox";
|
||||
import ToolBox from "../../components/Panels/ProviderAwareToolBox";
|
||||
import SidePanel from "../../components/Panels/SidePanel";
|
||||
import { mountVueComponent } from "../../utils/mountVueComponent";
|
||||
|
||||
@@ -23,7 +23,9 @@ const ToolPanel = Backbone.View.extend({
|
||||
currentPanel: ToolBox,
|
||||
currentPanelProperties: {
|
||||
storedWorkflowMenuEntries: Galaxy.config.stored_workflow_menu_entries,
|
||||
toolbox: Galaxy.config.toolbox,
|
||||
// TODO: find a way to seed to Vuex store's default panel view with data
|
||||
// or just stop including it in the response.
|
||||
// toolbox: Galaxy.config.toolbox,
|
||||
},
|
||||
},
|
||||
el
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import createCache from "vuex-cache";
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
|
||||
import config from "config";
|
||||
|
||||
import { gridSearchStore } from "./gridSearchStore";
|
||||
@@ -20,6 +22,7 @@ import { toolStore } from "./toolStore";
|
||||
import { datasetPathDestinationStore } from "./datasetPathDestinationStore";
|
||||
import { datasetExtFilesStore } from "./datasetExtFilesStore";
|
||||
import { jobStore } from "./jobStore";
|
||||
import { panelStore } from "./panelStore";
|
||||
|
||||
// beta features
|
||||
import { historyStore as betaHistoryStore } from "components/History/model/historyStore";
|
||||
@@ -29,13 +32,18 @@ import { syncVuextoGalaxy } from "./syncVuextoGalaxy";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const panelsState = createPersistedState({
|
||||
paths: ["panels"],
|
||||
});
|
||||
|
||||
export function createStore() {
|
||||
const storeConfig = {
|
||||
plugins: [createCache()],
|
||||
plugins: [createCache(), panelsState],
|
||||
modules: {
|
||||
user: userStore,
|
||||
config: configStore,
|
||||
betaHistory: betaHistoryStore,
|
||||
panels: panelStore,
|
||||
|
||||
// TODO: please namespace all store modules
|
||||
gridSearch: gridSearchStore,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export const state = {
|
||||
panel: {},
|
||||
currentPanelView: null,
|
||||
};
|
||||
|
||||
import Vue from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
const getters = {
|
||||
panel: (state) => (panelView) => {
|
||||
return state.panel[panelView] || [];
|
||||
},
|
||||
currentPanel: (state) => {
|
||||
const effectiveView = state.currentPanelView || "default";
|
||||
const val = state.panel[effectiveView] || [];
|
||||
return val;
|
||||
},
|
||||
currentPanelView: (state) => {
|
||||
return state.currentPanelView;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
initCurrentPanelView: async ({ commit, state }, siteDefaultPanelView) => {
|
||||
const panelView = state.currentPanelView || siteDefaultPanelView;
|
||||
if (state.currentPanelView == null) {
|
||||
commit("setCurrentPanelView", { panelView });
|
||||
}
|
||||
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=true&view=${panelView}`);
|
||||
commit("savePanelView", { panelView, panel: data });
|
||||
},
|
||||
setCurrentPanelView: async ({ commit }, panelView) => {
|
||||
commit("setCurrentPanelView", { panelView });
|
||||
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=true&view=${panelView}`);
|
||||
commit("savePanelView", { panelView, panel: data });
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
savePanelView: (state, { panelView, panel }) => {
|
||||
Vue.set(state.panel, panelView, panel);
|
||||
},
|
||||
setCurrentPanelView: (state, { panelView }) => {
|
||||
state.currentPanelView = panelView;
|
||||
},
|
||||
};
|
||||
|
||||
export const panelStore = {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -13351,6 +13351,11 @@ shellwords@^0.1.1:
|
||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||
|
||||
shvl@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1"
|
||||
integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==
|
||||
|
||||
side-channel@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
|
||||
@@ -15310,6 +15315,14 @@ vuex-persist@^3.1.3:
|
||||
deepmerge "^4.2.2"
|
||||
flatted "^3.0.5"
|
||||
|
||||
vuex-persistedstate@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.0.0.tgz#ed82f266ca98c869a2aad9cb9880c2f608c05f3a"
|
||||
integrity sha512-jDs+awbV9YD2A2F6S5zgtYq1Bjd8v0YldOK6HPv1EJZzGMse0FtZTREfXvA7zlVfq9MpmSZJNmYQVylfpZ5znQ==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
shvl "^2.0.3"
|
||||
|
||||
vuex@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb34e7fceeaebde45"
|
||||
|
||||
@@ -60,6 +60,7 @@ docopt==0.6.2
|
||||
docutils==0.16; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
|
||||
dogpile.cache==1.1.2; python_version >= "3.6"
|
||||
ecdsa==0.14.1; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
|
||||
edam-ontology==1.25.2
|
||||
fabric3==1.14.post1
|
||||
fastapi-utils==0.2.1; python_version >= "3.6" and python_version < "4.0"
|
||||
fastapi==0.65.2; python_version >= "3.6"
|
||||
|
||||
@@ -55,6 +55,7 @@ docopt==0.6.2
|
||||
docutils==0.16; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
|
||||
dogpile.cache==1.1.2; python_version >= "3.6"
|
||||
ecdsa==0.14.1; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
|
||||
edam-ontology==1.25.2
|
||||
fabric3==1.14.post1
|
||||
fastapi-utils==0.2.1; python_version >= "3.6" and python_version < "4.0"
|
||||
fastapi==0.65.2; python_version >= "3.6"
|
||||
|
||||
@@ -200,6 +200,8 @@ class ConfigSerializer(base.ModelSerializer):
|
||||
'python': _defaults_to((sys.version_info.major, sys.version_info.minor)),
|
||||
'select_type_workflow_threshold': _use_config,
|
||||
'file_sources_configured': lambda config, key, **context: self.app.file_sources.custom_sources_configured,
|
||||
'panel_views': lambda config, key, **content: self.app.toolbox.panel_view_dicts(),
|
||||
'default_panel_view': _use_config,
|
||||
'upload_from_form_button': _use_config,
|
||||
'release_doc_base_url': _use_config,
|
||||
'user_library_import_dir_available': lambda config, key, **context: bool(config.get('user_library_import_dir')),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import io
|
||||
import os
|
||||
from typing import Dict, Optional, TextIO
|
||||
|
||||
try:
|
||||
from edam_ontology.streams import tabular_stream
|
||||
except ImportError:
|
||||
tabular_stream = None
|
||||
|
||||
EDAM_PREFIX = 'http://edamontology.org/'
|
||||
COLUMN_TERM = 0
|
||||
COLUMN_LABEL = 1
|
||||
COLUMN_PARENTS = 7
|
||||
|
||||
ROOT_OPERATION = 'operation_0004'
|
||||
ROOT_TOPIC = 'topic_0003'
|
||||
|
||||
|
||||
def load_edam_tree(path: Optional[str] = None):
|
||||
if path is not None:
|
||||
assert os.path.exists(path), f"Failed to load EDAM tabular data at [{path}] path does not exist."
|
||||
handle = io.open(path, "r")
|
||||
else:
|
||||
assert tabular_stream is not None, "Failed to load optional import from edam-onotology package, install using [pip install edam-ontology]."
|
||||
handle = tabular_stream()
|
||||
return load_edam_tree_from_tsv_stream(handle)
|
||||
|
||||
|
||||
def load_edam_tree_from_tsv_stream(tsv_stream: TextIO):
|
||||
edam: Dict[str, Dict] = {}
|
||||
|
||||
def _recurse_edam_parents(term, path=None):
|
||||
if edam[term]['parents'] and len(edam[term]['parents']) > 0:
|
||||
for parent in edam[term]['parents']:
|
||||
yield from _recurse_edam_parents(parent, path + [parent])
|
||||
else:
|
||||
yield path
|
||||
|
||||
for line in tsv_stream.readlines():
|
||||
fields = line.split('\t')
|
||||
|
||||
term = fields[COLUMN_TERM]
|
||||
if not term.startswith(EDAM_PREFIX):
|
||||
continue
|
||||
|
||||
term_id = term[len(EDAM_PREFIX):]
|
||||
|
||||
# Only care about formats and operations
|
||||
if not (term_id.startswith('operation_') or term_id.startswith('topic_')):
|
||||
continue
|
||||
|
||||
parents = fields[COLUMN_PARENTS].split('|')
|
||||
edam[term_id] = {
|
||||
'label': fields[COLUMN_LABEL], # preferred label
|
||||
'parents': [x[len(EDAM_PREFIX):] for x in parents if x.startswith(EDAM_PREFIX)],
|
||||
}
|
||||
|
||||
for term in sorted(edam.keys()):
|
||||
tails = []
|
||||
for x in _recurse_edam_parents(term, path=[]):
|
||||
if x[-2:] not in tails:
|
||||
tails.append(x[-2:])
|
||||
edam[term]['path'] = tails
|
||||
|
||||
return edam
|
||||
|
||||
|
||||
__all__ = ('load_edam_tree', 'ROOT_OPERATION', 'ROOT_TOPIC')
|
||||
@@ -3,9 +3,9 @@ import logging
|
||||
import os
|
||||
import string
|
||||
import time
|
||||
import urllib.request
|
||||
from collections import namedtuple
|
||||
from errno import ENOENT
|
||||
from typing import Dict, List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from markupsafe import escape
|
||||
@@ -14,6 +14,7 @@ from galaxy.exceptions import (
|
||||
ConfigurationError,
|
||||
MessageException,
|
||||
ObjectNotFound,
|
||||
RequestParameterInvalidException,
|
||||
)
|
||||
from galaxy.tool_util.deps import (
|
||||
build_dependency_manager,
|
||||
@@ -23,10 +24,8 @@ from galaxy.tool_util.loader_directory import looks_like_a_tool
|
||||
from galaxy.util import (
|
||||
etree,
|
||||
ExecutionTimer,
|
||||
get_charset_from_http_headers,
|
||||
listify,
|
||||
parse_xml,
|
||||
stream_to_open_named_file,
|
||||
string_as_bool,
|
||||
unicodify,
|
||||
)
|
||||
@@ -46,6 +45,14 @@ from .parser import (
|
||||
get_toolbox_parser
|
||||
)
|
||||
from .tags import tool_tag_manager
|
||||
from .views.edam import EdamPanelMode, EdamToolPanelView
|
||||
from .views.interface import (
|
||||
ToolBoxRegistry,
|
||||
ToolPanelView,
|
||||
ToolPanelViewModel,
|
||||
ToolPanelViewModelType,
|
||||
)
|
||||
from .views.static import StaticToolPanelView
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,24 +78,43 @@ class ToolConfRepository(_ToolConfRepository):
|
||||
return self.tool_path, self.repository_path
|
||||
|
||||
|
||||
class ToolBoxRegistryImpl(ToolBoxRegistry):
|
||||
"""View of ToolBox provided to ToolPanelView to reason about tools loaded."""
|
||||
|
||||
def __init__(self, toolbox: 'AbstractToolBox'):
|
||||
self.__toolbox = toolbox
|
||||
|
||||
def has_tool(self, tool_id: str) -> bool:
|
||||
return tool_id in self.__toolbox._tools_by_id
|
||||
|
||||
def get_tool(self, tool_id: str):
|
||||
return self.__toolbox.get_tool(tool_id)
|
||||
|
||||
def get_workflow(self, id: str):
|
||||
return self.__toolbox._workflows_by_id[id]
|
||||
|
||||
def add_tool_to_tool_panel_view(self, tool, tool_panel_component) -> None:
|
||||
self.__toolbox.add_tool_to_tool_panel_view(tool, tool_panel_component)
|
||||
|
||||
|
||||
class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
"""
|
||||
Abstract container for managing a ToolPanel - containing tools and
|
||||
workflows optionally in labelled sections.
|
||||
"""
|
||||
|
||||
def __init__(self, config_filenames, tool_root_dir, app, save_integrated_tool_panel=True):
|
||||
def __init__(self, config_filenames, tool_root_dir, app, view_sources=None, default_panel_view="default", save_integrated_tool_panel=True):
|
||||
"""
|
||||
Create a toolbox from the config files named by `config_filenames`, using
|
||||
`tool_root_dir` as the base directory for finding individual tool config files.
|
||||
"""
|
||||
self._default_panel_view = default_panel_view
|
||||
# The _dynamic_tool_confs list contains dictionaries storing
|
||||
# information about the tools defined in each shed-related
|
||||
# shed_tool_conf.xml file.
|
||||
self._dynamic_tool_confs = []
|
||||
self._tools_by_id = {}
|
||||
self._tools_by_uuid = {}
|
||||
self._integrated_section_by_tool = {}
|
||||
# Tool lineages can contain chains of related tools with different ids
|
||||
# so each will be present once in the above dictionary. The following
|
||||
# dictionary can instead hold multiple tools with different versions.
|
||||
@@ -113,64 +139,53 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
self._filter_factory = FilterFactory(self)
|
||||
self._tool_tag_manager = tool_tag_manager(app)
|
||||
self._init_tools_from_configs(config_filenames)
|
||||
self._load_edam()
|
||||
|
||||
if self.app.name == 'galaxy' and self._integrated_tool_panel_config_has_contents:
|
||||
# Load self._tool_panel based on the order in self._integrated_tool_panel.
|
||||
if self.app.config.enable_beta_edam_toolbox:
|
||||
self._load_tool_panel_edam()
|
||||
else:
|
||||
self._load_tool_panel()
|
||||
self._load_tool_panel()
|
||||
|
||||
toolbox = self
|
||||
|
||||
class DefaultToolPanelView(ToolPanelView):
|
||||
|
||||
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
|
||||
return toolbox._tool_panel
|
||||
|
||||
def to_model(self) -> ToolPanelViewModel:
|
||||
model_id = "default"
|
||||
name = "Full Tool Panel"
|
||||
description = "Galaxy's fully configured toolbox panel with all sections, tools, and configured workflows loaded."
|
||||
view_type = ToolPanelViewModelType.default_type
|
||||
model_class = DefaultToolPanelView.__class__.__name__
|
||||
return ToolPanelViewModel(
|
||||
id=model_id,
|
||||
name=name,
|
||||
description=description,
|
||||
model_class=model_class,
|
||||
view_type=view_type,
|
||||
searchable=True,
|
||||
)
|
||||
|
||||
tool_panel_views_list = [
|
||||
DefaultToolPanelView(),
|
||||
]
|
||||
|
||||
for edam_view in listify(self.app.config.edam_panel_views):
|
||||
mode = EdamPanelMode[edam_view]
|
||||
tool_panel_views_list.append(EdamToolPanelView(self.app.config.edam_toolbox_ontology_path, mode=mode))
|
||||
|
||||
if view_sources is not None:
|
||||
for definition in view_sources.get_definitions():
|
||||
tool_panel_views_list.append(StaticToolPanelView(definition))
|
||||
|
||||
self._tool_panel_views = {}
|
||||
for tool_panel_view in tool_panel_views_list:
|
||||
self._tool_panel_views[tool_panel_view.to_model().id] = tool_panel_view
|
||||
|
||||
if self.app.name == 'galaxy':
|
||||
self._load_tool_panel_views()
|
||||
if save_integrated_tool_panel:
|
||||
self._save_integrated_tool_panel()
|
||||
|
||||
def _recurse_edam_parents(self, term, path=None):
|
||||
log.debug(f"term:{term} path:{path} parents:{self.edam[term]['parents']}")
|
||||
if self.edam[term]['parents'] and len(self.edam[term]['parents']) > 0:
|
||||
for parent in self.edam[term]['parents']:
|
||||
yield from self._recurse_edam_parents(parent, path + [parent])
|
||||
else:
|
||||
yield path
|
||||
|
||||
def _load_edam(self):
|
||||
if not self.app.config.enable_beta_edam_toolbox:
|
||||
return
|
||||
log.debug('Loading EDAM Terms')
|
||||
|
||||
if not os.path.exists(self.app.config.beta_edam_toolbox_ontology_path):
|
||||
log.debug('EDAM ontology file not present, downloading')
|
||||
|
||||
page = urllib.request.urlopen('https://edamontology.org/EDAM.tsv')
|
||||
stream_to_open_named_file(page, os.open(self.app.config.beta_edam_toolbox_ontology_path, os.O_WRONLY | os.O_CREAT), None, source_encoding=get_charset_from_http_headers(page.headers))
|
||||
|
||||
log.debug('Processing EDAM Terms')
|
||||
with open(self.app.config.beta_edam_toolbox_ontology_path, 'r') as handle:
|
||||
log.debug(f'Processing {handle}')
|
||||
self.edam = {}
|
||||
for line in handle.readlines():
|
||||
fields = line.split('\t')
|
||||
if not fields[0].startswith('http://edamontology.org/'):
|
||||
continue
|
||||
term_id = fields[0][len('http://edamontology.org/'):]
|
||||
|
||||
# Only care about formats and operations
|
||||
if not (term_id.startswith('operation_') or term_id.startswith('topic_')):
|
||||
continue
|
||||
|
||||
parents = fields[7].split('|')
|
||||
self.edam[term_id] = {
|
||||
'label': fields[1], # preferred label
|
||||
'parents': [x[len('http://edamontology.org/'):] for x in parents if x.startswith('http://edamontology.org/')],
|
||||
}
|
||||
log.debug(f'Loaded term {term_id} => {fields[1]}')
|
||||
|
||||
for term in sorted(self.edam.keys()):
|
||||
tails = []
|
||||
for x in self._recurse_edam_parents(term, path=[]):
|
||||
if x[-2:] not in tails:
|
||||
tails.append(x[-2:])
|
||||
self.edam[term]['path'] = tails
|
||||
|
||||
def create_tool(self, config_file, tool_shed_repository=None, guid=None, **kwds):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -289,6 +304,16 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
|
||||
return None
|
||||
|
||||
def panel_views(self) -> List[ToolPanelViewModel]:
|
||||
return [v.to_model() for v in self._tool_panel_views.values()]
|
||||
|
||||
def panel_view_dicts(self) -> Dict[str, Dict]:
|
||||
return {m.id: m.dict() for m in self.panel_views()}
|
||||
|
||||
def panel_has_tool(self, tool, panel_view_id):
|
||||
panel_view_rendered = self._tool_panel_view_rendered[panel_view_id]
|
||||
return panel_view_rendered.has_item_recursive(tool)
|
||||
|
||||
def load_dynamic_tool(self, dynamic_tool):
|
||||
if not dynamic_tool.active:
|
||||
return None
|
||||
@@ -370,13 +395,9 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
log.debug(f"Loading new tool panel section: {str(tool_section.name)}")
|
||||
return tool_section
|
||||
|
||||
def get_integrated_section_for_tool(self, tool):
|
||||
def get_section_for_tool(self, tool):
|
||||
tool_id = tool.id
|
||||
|
||||
if tool_id in self._integrated_section_by_tool:
|
||||
return self._integrated_section_by_tool[tool_id]
|
||||
|
||||
return None, None
|
||||
return self._tool_panel.get_section_for_tool_id(tool_id)
|
||||
|
||||
def __resolve_tool_path(self, tool_path, config_filename):
|
||||
if not tool_path:
|
||||
@@ -389,7 +410,12 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
tool_path = string.Template(tool_path).safe_substitute(tool_path_vars)
|
||||
return tool_path
|
||||
|
||||
def __add_tool_to_tool_panel(self, tool, panel_component, section=False):
|
||||
def add_tool_to_tool_panel_view(self, tool, view_panel_component):
|
||||
self.__add_tool_to_tool_panel(tool, view_panel_component)
|
||||
|
||||
def __add_tool_to_tool_panel(self, tool, panel_component, section=None):
|
||||
if section is None:
|
||||
section = isinstance(panel_component, ToolSection)
|
||||
# See if a version of this tool is already loaded into the tool panel.
|
||||
# The value of panel_component will be a ToolSection (if the value of
|
||||
# section=True) or self._tool_panel (if section=False).
|
||||
@@ -456,140 +482,11 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
if log_msg and (not hasattr(self.app, 'tool_cache') or tool_id in self.app.tool_cache._new_tool_ids):
|
||||
log.debug(log_msg)
|
||||
|
||||
def _get_edam_sec(self, tool):
|
||||
edam = tool.edam_operations + tool.edam_topics
|
||||
if len(edam) > 0:
|
||||
for term in edam:
|
||||
yield term
|
||||
else:
|
||||
yield 'uncategorized'
|
||||
|
||||
def _get_section(self, sec_id, sec_nm):
|
||||
if sec_id not in self._tool_panel:
|
||||
section = ToolSection({'id': sec_id, 'name': sec_nm, 'version': ''})
|
||||
self._tool_panel[sec_id] = section
|
||||
else:
|
||||
section = self._tool_panel[sec_id]
|
||||
return section
|
||||
|
||||
def _edam_children_of(self, parentTerm):
|
||||
for term in self.edam.keys():
|
||||
if parentTerm in self.edam[term]['parents']:
|
||||
yield term
|
||||
|
||||
def _load_tool_panel_edam(self):
|
||||
execution_timer = ExecutionTimer()
|
||||
|
||||
# Find the children of the top level topics
|
||||
operations = ['operation_0004'] + list(self._edam_children_of('operation_0004'))
|
||||
topics = ['topic_0003'] + list(self._edam_children_of('topic_0003'))
|
||||
|
||||
# Sort them (by english label)
|
||||
# operations = sorted(operations, key=lambda x: self.edam[x]['label'])
|
||||
# topics = sorted(topics, key=lambda x: self.edam[x]['label'])
|
||||
|
||||
# Convert these to list of dicts, wherein we'll add our tools/etc.
|
||||
operations = {
|
||||
x: {}
|
||||
for x in operations
|
||||
}
|
||||
topics = {
|
||||
x: {}
|
||||
for x in topics
|
||||
}
|
||||
uncategorized = []
|
||||
|
||||
for key, item_type, val in self._integrated_tool_panel.panel_items_iter():
|
||||
if item_type == panel_item_types.TOOL:
|
||||
tool_id = key.replace('tool_', '', 1)
|
||||
if tool_id in self._tools_by_id:
|
||||
if tool_id in self._tools_by_id:
|
||||
for term in self._get_edam_sec(val):
|
||||
if term == 'uncategorized':
|
||||
uncategorized.append((tool_id, key, val, val.name))
|
||||
else:
|
||||
for path in self.edam[term]['path']:
|
||||
if len(path) == 1:
|
||||
t = term
|
||||
else:
|
||||
t = path[0]
|
||||
|
||||
if path[0].startswith('operation_'):
|
||||
operations[t][tool_id] = (term, tool_id, key, val, val.name)
|
||||
elif path[0].startswith('topic_'):
|
||||
topics[t][tool_id] = (term, tool_id, key, val, val.name)
|
||||
# elif item_type == panel_item_types.WORKFLOW:
|
||||
# workflow_id = key.replace('workflow_', '', 1)
|
||||
# if workflow_id in self._workflows_by_id:
|
||||
# workflow = self._workflows_by_id[workflow_id]
|
||||
# self._tool_panel[key] = workflow
|
||||
# log.debug(f"Loaded workflow: {workflow_id} {workflow.name}")
|
||||
elif item_type == panel_item_types.SECTION:
|
||||
for section_key, section_item_type, section_val in val.panel_items_iter():
|
||||
if section_item_type == panel_item_types.TOOL:
|
||||
tool_id = section_key.replace('tool_', '', 1)
|
||||
if tool_id in self._tools_by_id:
|
||||
for term in self._get_edam_sec(section_val):
|
||||
if term == 'uncategorized':
|
||||
uncategorized.append((tool_id, key, section_val, val.name))
|
||||
else:
|
||||
for path in self.edam[term]['path']:
|
||||
if len(path) == 1:
|
||||
t = term
|
||||
else:
|
||||
t = path[0]
|
||||
|
||||
if path[0].startswith('operation_'):
|
||||
operations[t][tool_id] = (term, tool_id, key, section_val, val.name)
|
||||
if path[0].startswith('topic_'):
|
||||
topics[t][tool_id] = (term, tool_id, key, section_val, val.name)
|
||||
# elif section_item_type == panel_item_types.WORKFLOW:
|
||||
# workflow_id = section_key.replace('workflow_', '', 1)
|
||||
# if workflow_id in self._workflows_by_id:
|
||||
# workflow = self._workflows_by_id[workflow_id]
|
||||
# section.elems[section_key] = workflow
|
||||
# log.debug(f"Loaded workflow: {workflow_id} {workflow.name}")
|
||||
log.debug("Loading tool panel finished %s", execution_timer)
|
||||
|
||||
for term in sorted(operations.keys(), key=lambda x: self._sort_edam_key(x)):
|
||||
if len(operations[term].keys()) == 0:
|
||||
continue
|
||||
|
||||
elem = etree.Element('label')
|
||||
elem.attrib['text'] = self.edam[term]['label']
|
||||
elem.attrib['id'] = term
|
||||
self._tool_panel[f"label_{term}"] = ToolSectionLabel(elem)
|
||||
|
||||
for (term, tool_id, key, val, val_name) in operations[term].values():
|
||||
section = self._get_section(term, self.edam[term]['label'])
|
||||
self.__add_tool_to_tool_panel(val, section, section=True)
|
||||
self._integrated_section_by_tool[tool_id] = key, val_name
|
||||
|
||||
for term in sorted(topics.keys(), key=lambda x: self._sort_edam_key(x)):
|
||||
if len(topics[term].keys()) == 0:
|
||||
continue
|
||||
|
||||
elem = etree.Element('label')
|
||||
elem.attrib['text'] = self.edam[term]['label']
|
||||
elem.attrib['id'] = term
|
||||
self._tool_panel[f"label_{term}"] = ToolSectionLabel(elem)
|
||||
|
||||
for (term, tool_id, key, val, val_name) in topics[term].values():
|
||||
section = self._get_section(term, self.edam[term]['label'])
|
||||
self.__add_tool_to_tool_panel(val, section, section=True)
|
||||
self._integrated_section_by_tool[tool_id] = key, val_name
|
||||
|
||||
section = self._get_section('uncategorized', 'Uncategorized')
|
||||
for (tool_id, key, val, val_name) in uncategorized:
|
||||
print(tool_id, key, val, val_name)
|
||||
self.__add_tool_to_tool_panel(val, section, section=True)
|
||||
self._integrated_section_by_tool[tool_id] = key, val_name
|
||||
|
||||
def _sort_edam_key(self, x):
|
||||
if x in ('operation_0004', 'topic_0003'):
|
||||
return f"!{x}"
|
||||
else:
|
||||
return self.edam[x]['label']
|
||||
def _load_tool_panel_views(self):
|
||||
self._tool_panel_view_rendered = {}
|
||||
registry = ToolBoxRegistryImpl(self)
|
||||
for key, view in self._tool_panel_views.items():
|
||||
self._tool_panel_view_rendered[key] = view.apply_view(self._integrated_tool_panel, registry)
|
||||
|
||||
def _load_tool_panel(self):
|
||||
execution_timer = ExecutionTimer()
|
||||
@@ -598,7 +495,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
tool_id = key.replace('tool_', '', 1)
|
||||
if tool_id in self._tools_by_id:
|
||||
self.__add_tool_to_tool_panel(val, self._tool_panel, section=False)
|
||||
self._integrated_section_by_tool[tool_id] = '', ''
|
||||
self._tool_panel.record_section_for_tool_id(tool_id, '', '')
|
||||
elif item_type == panel_item_types.WORKFLOW:
|
||||
workflow_id = key.replace('workflow_', '', 1)
|
||||
if workflow_id in self._workflows_by_id:
|
||||
@@ -620,7 +517,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
tool_id = section_key.replace('tool_', '', 1)
|
||||
if tool_id in self._tools_by_id:
|
||||
self.__add_tool_to_tool_panel(section_val, section, section=True)
|
||||
self._integrated_section_by_tool[tool_id] = key, val.name
|
||||
self._tool_panel.record_section_for_tool_id(tool_id, key, val.name)
|
||||
elif section_item_type == panel_item_types.WORKFLOW:
|
||||
workflow_id = section_key.replace('workflow_', '', 1)
|
||||
if workflow_id in self._workflows_by_id:
|
||||
@@ -930,7 +827,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
tool._lineage = self._lineage_map.register(tool)
|
||||
self.register_tool(tool)
|
||||
if load_panel_dict:
|
||||
self.__add_tool_to_tool_panel(tool, panel_dict, section=isinstance(panel_dict, ToolSection))
|
||||
self.__add_tool_to_tool_panel(tool, panel_dict)
|
||||
|
||||
def _load_workflow_tag_set(self, item, panel_dict, integrated_panel_dict, load_panel_dict, index=None):
|
||||
try:
|
||||
@@ -984,7 +881,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
if section_item_type == panel_item_types.TOOL:
|
||||
if section_item:
|
||||
tool_id = section_key.replace('tool_', '', 1)
|
||||
self._integrated_section_by_tool[tool_id] = integrated_section.id, integrated_section.name
|
||||
self._tool_panel.record_section_for_tool_id(tool_id, integrated_section.id, integrated_section.name)
|
||||
|
||||
if load_panel_dict:
|
||||
self._tool_panel[key] = section
|
||||
@@ -1007,10 +904,8 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
integrated_elems[key] = tool
|
||||
|
||||
if async_load:
|
||||
if self.app.config.enable_beta_edam_toolbox:
|
||||
self._load_tool_panel_edam()
|
||||
else:
|
||||
self._load_tool_panel()
|
||||
self._load_tool_panel()
|
||||
self._load_tool_panel_views()
|
||||
self._save_integrated_tool_panel()
|
||||
return tool.id
|
||||
except Exception:
|
||||
@@ -1141,15 +1036,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
new_tool.installed_changeset_revision = old_tool.installed_changeset_revision
|
||||
new_tool.old_id = old_tool.old_id
|
||||
# Replace old_tool with new_tool in self._tool_panel
|
||||
tool_key = f"tool_{tool_id}"
|
||||
for key, val in self._tool_panel.items():
|
||||
if key == tool_key:
|
||||
self._tool_panel[key] = new_tool
|
||||
break
|
||||
elif key.startswith('section'):
|
||||
if tool_key in val.elems:
|
||||
self._tool_panel[key].elems[tool_key] = new_tool
|
||||
break
|
||||
self._tool_panel.replace_tool_for_id(tool_id, new_tool)
|
||||
# (Re-)Register the reloaded tool, this will handle
|
||||
# _tools_by_id and _tool_versions_by_id
|
||||
self.register_tool(new_tool)
|
||||
@@ -1175,15 +1062,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
if tool_cache:
|
||||
tool_cache.expire_tool(tool_id)
|
||||
if remove_from_panel:
|
||||
tool_key = f"tool_{tool_id}"
|
||||
for key, val in self._tool_panel.items():
|
||||
if key == tool_key:
|
||||
del self._tool_panel[key]
|
||||
break
|
||||
elif key.startswith('section'):
|
||||
if tool_key in val.elems:
|
||||
del self._tool_panel[key].elems[tool_key]
|
||||
break
|
||||
self._tool_panel.remove_tool(tool_id)
|
||||
if tool_id in self.data_manager_tools:
|
||||
del self.data_manager_tools[tool_id]
|
||||
# TODO: do we need to manually remove from the integrated panel here?
|
||||
@@ -1195,6 +1074,10 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
return message, status
|
||||
|
||||
def get_sections(self):
|
||||
"""Return section id and name pairs.
|
||||
|
||||
Only used by build_tool_panel_section_select_field in tool shed client code.
|
||||
"""
|
||||
for v in self._tool_panel.values():
|
||||
if isinstance(v, ToolSection):
|
||||
yield (v.id, v.name)
|
||||
@@ -1226,11 +1109,16 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
stored = self.app.model.context.query(self.app.model.StoredWorkflow).get(id)
|
||||
return stored.latest_workflow
|
||||
|
||||
def tool_panel_contents(self, trans, **kwds):
|
||||
def tool_panel_contents(self, trans, view=None, **kwds):
|
||||
""" Filter tool_panel contents for displaying for user.
|
||||
"""
|
||||
if view is None:
|
||||
view = self._default_panel_view
|
||||
if view not in self._tool_panel_view_rendered:
|
||||
raise RequestParameterInvalidException(f"No panel view {view} found.")
|
||||
filter_method = self._build_filter_method(trans)
|
||||
for _, item_type, elt in self._tool_panel.panel_items_iter():
|
||||
tool_panel_view = self._tool_panel_view_rendered[view]
|
||||
for _, item_type, elt in tool_panel_view.panel_items_iter():
|
||||
elt = filter_method(elt, item_type)
|
||||
if elt:
|
||||
yield elt
|
||||
@@ -1257,14 +1145,14 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
|
||||
self._tool_to_dict_cache_admin[tool.id] = to_dict
|
||||
return to_dict
|
||||
|
||||
def to_dict(self, trans, in_panel=True, tool_help=False, **kwds):
|
||||
def to_dict(self, trans, in_panel=True, tool_help=False, view=None, **kwds):
|
||||
"""
|
||||
Create a dictionary representation of the toolbox.
|
||||
Uses primitive cache for toolbox-specific tool 'to_dict's.
|
||||
"""
|
||||
rval = []
|
||||
if in_panel:
|
||||
panel_elts = list(self.tool_panel_contents(trans, **kwds))
|
||||
panel_elts = list(self.tool_panel_contents(trans, view=view, **kwds))
|
||||
for elt in panel_elts:
|
||||
# Only use cache for objects that are Tools.
|
||||
if hasattr(elt, "tool_type"):
|
||||
@@ -1382,8 +1270,8 @@ class BaseGalaxyToolBox(AbstractToolBox):
|
||||
shouldn't really depend on.
|
||||
"""
|
||||
|
||||
def __init__(self, config_filenames, tool_root_dir, app, save_integrated_tool_panel=True):
|
||||
super().__init__(config_filenames, tool_root_dir, app, save_integrated_tool_panel)
|
||||
def __init__(self, config_filenames, tool_root_dir, app, view_sources=None, default_panel_view=None, save_integrated_tool_panel=True):
|
||||
super().__init__(config_filenames, tool_root_dir, app, view_sources, default_panel_view, save_integrated_tool_panel)
|
||||
old_toolbox = getattr(app, 'toolbox', None)
|
||||
if old_toolbox:
|
||||
self.dependency_manager = old_toolbox.dependency_manager
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
from galaxy.util.tool_version import remove_version_from_guid
|
||||
from .interface import ToolLineage
|
||||
|
||||
@@ -7,16 +9,17 @@ class LineageMap:
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.lineage_map = {}
|
||||
self.lineage_map: Dict[str, ToolLineage] = {}
|
||||
self.app = app
|
||||
|
||||
def register(self, tool):
|
||||
def register(self, tool) -> ToolLineage:
|
||||
tool_id = tool.id
|
||||
versionless_tool_id = remove_version_from_guid(tool_id)
|
||||
lineage = self.lineage_map.get(versionless_tool_id)
|
||||
if not lineage:
|
||||
lineage: ToolLineage
|
||||
if versionless_tool_id not in self.lineage_map:
|
||||
lineage = ToolLineage.from_tool(tool)
|
||||
else:
|
||||
lineage = self.lineage_map[versionless_tool_id]
|
||||
# A lineage for a tool with the same versionless_tool_id exists,
|
||||
# but this lineage may not have the current tools' version,
|
||||
# so we add tool.version to the lineage
|
||||
@@ -27,7 +30,7 @@ class LineageMap:
|
||||
self.lineage_map[tool_id] = lineage
|
||||
return self.lineage_map[tool_id]
|
||||
|
||||
def get(self, tool_id):
|
||||
def get(self, tool_id) -> Optional[ToolLineage]:
|
||||
"""
|
||||
Get lineage for `tool_id`.
|
||||
|
||||
@@ -46,7 +49,7 @@ class LineageMap:
|
||||
self.lineage_map[tool_id] = lineage
|
||||
return self.lineage_map.get(tool_id)
|
||||
|
||||
def _get_versionless(self, tool_id):
|
||||
def _get_versionless(self, tool_id) -> Optional[ToolLineage]:
|
||||
versionless_tool_id = remove_version_from_guid(tool_id)
|
||||
return self.lineage_map.get(versionless_tool_id, None)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import threading
|
||||
from typing import Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import packaging.version
|
||||
from sortedcontainers import SortedSet
|
||||
@@ -44,13 +44,13 @@ class ToolLineage:
|
||||
self.tool_versions = SortedSet(key=packaging.version.parse)
|
||||
|
||||
@property
|
||||
def tool_ids(self):
|
||||
def tool_ids(self) -> List[str]:
|
||||
versionless_tool_id = remove_version_from_guid(self.tool_id)
|
||||
tool_id = versionless_tool_id or self.tool_id
|
||||
return [f"{tool_id}/{version}" for version in self.tool_versions]
|
||||
|
||||
@staticmethod
|
||||
def from_tool(tool):
|
||||
def from_tool(tool) -> 'ToolLineage':
|
||||
tool_id = tool.id
|
||||
lineages_by_id = ToolLineage.lineages_by_id
|
||||
with ToolLineage.lock:
|
||||
@@ -60,7 +60,7 @@ class ToolLineage:
|
||||
lineage.register_version(tool.version)
|
||||
return lineage
|
||||
|
||||
def register_version(self, tool_version):
|
||||
def register_version(self, tool_version) -> None:
|
||||
assert tool_version is not None
|
||||
self.tool_versions.add(str(tool_version))
|
||||
|
||||
@@ -71,12 +71,12 @@ class ToolLineage:
|
||||
"""
|
||||
return [ToolLineageVersion(tool_id, tool_version) for tool_id, tool_version in zip(self.tool_ids, self.tool_versions)]
|
||||
|
||||
def get_version_ids(self, reverse=False):
|
||||
def get_version_ids(self, reverse=False) -> List[str]:
|
||||
if reverse:
|
||||
return list(reversed(self.tool_ids))
|
||||
return self.tool_ids
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return dict(
|
||||
tool_id=self.tool_id,
|
||||
tool_versions=list(self.tool_versions),
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
|
||||
from galaxy.util import bunch
|
||||
from galaxy.util.dictifiable import Dictifiable
|
||||
from galaxy.util.odict import odict
|
||||
from .parser import ensure_tool_conf_item
|
||||
|
||||
|
||||
panel_item_types = bunch.Bunch(
|
||||
TOOL="TOOL",
|
||||
WORKFLOW="WORKFLOW",
|
||||
SECTION="SECTION",
|
||||
LABEL="LABEL",
|
||||
)
|
||||
class panel_item_types(str, Enum):
|
||||
TOOL = "TOOL"
|
||||
WORKFLOW = "WORKFLOW"
|
||||
SECTION = "SECTION"
|
||||
LABEL = "LABEL"
|
||||
|
||||
|
||||
class HasPanelItems:
|
||||
@@ -65,7 +64,7 @@ class ToolSection(Dictifiable, HasPanelItems):
|
||||
copy.name = self.name
|
||||
copy.id = self.id
|
||||
copy.version = self.version
|
||||
copy.elems = self.elems.copy()
|
||||
copy.elems.update(self.elems)
|
||||
return copy
|
||||
|
||||
def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None):
|
||||
@@ -115,62 +114,143 @@ class ToolPanelElements(odict, HasPanelItems):
|
||||
""" Represents an ordered dictionary of tool entries - abstraction
|
||||
used both by tool panel itself (normal and integrated) and its sections.
|
||||
"""
|
||||
_section_by_tool: Dict[str, Tuple[str, str]] = {}
|
||||
|
||||
def update_or_append(self, index, key, value):
|
||||
def record_section_for_tool_id(self, tool_id: str, key: str, val: str):
|
||||
self._section_by_tool[tool_id] = (key, val)
|
||||
|
||||
def get_section_for_tool_id(self, tool_id: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
if tool_id in self._section_by_tool:
|
||||
return self._section_by_tool[tool_id]
|
||||
return (None, None)
|
||||
|
||||
def replace_tool_for_id(self, tool_id: str, new_tool) -> None:
|
||||
tool_key = f"tool_{tool_id}"
|
||||
for key, val in self.items():
|
||||
if key == tool_key:
|
||||
self[key] = new_tool
|
||||
break
|
||||
elif key.startswith('section'):
|
||||
if tool_key in val.elems:
|
||||
self[key].elems[tool_key] = new_tool
|
||||
break
|
||||
|
||||
def get_or_create_section(self, sec_id: str, sec_nm: str) -> ToolSection:
|
||||
if sec_id not in self:
|
||||
section = ToolSection({'id': sec_id, 'name': sec_nm, 'version': ''})
|
||||
self[sec_id] = section
|
||||
else:
|
||||
section = self[sec_id]
|
||||
return section
|
||||
|
||||
def remove_tool(self, tool_id: str) -> None:
|
||||
tool_key = f"tool_{tool_id}"
|
||||
for key, val in self.items():
|
||||
if key == tool_key:
|
||||
del self[key]
|
||||
break
|
||||
elif key.startswith('section'):
|
||||
if tool_key in val.elems:
|
||||
del self[key].elems[tool_key]
|
||||
break
|
||||
|
||||
def update_or_append(self, index: int, key: str, value) -> None:
|
||||
if key in self or index is None:
|
||||
self[key] = value
|
||||
else:
|
||||
self.insert(index, key, value)
|
||||
|
||||
def get_label(self, label):
|
||||
def get_label(self, label: str) -> Optional[ToolSection]:
|
||||
for element in self.values():
|
||||
if isinstance(element, ToolSection) and element.name == label:
|
||||
return element
|
||||
return None
|
||||
|
||||
def has_tool_with_id(self, tool_id):
|
||||
def has_tool_with_id(self, tool_id: str) -> bool:
|
||||
key = f'tool_{tool_id}'
|
||||
return key in self
|
||||
|
||||
def replace_tool(self, previous_tool_id, new_tool_id, tool):
|
||||
def replace_tool(self, previous_tool_id: str, new_tool_id: str, tool) -> None:
|
||||
previous_key = f'tool_{previous_tool_id}'
|
||||
new_key = f'tool_{new_tool_id}'
|
||||
index = self.keys().index(previous_key)
|
||||
del self[previous_key]
|
||||
self.insert(index, new_key, tool)
|
||||
|
||||
def index_of_tool_id(self, tool_id):
|
||||
def index_of_tool_id(self, tool_id: str) -> Optional[int]:
|
||||
query_key = f'tool_{tool_id}'
|
||||
for index, target_key in enumerate(self.keys()):
|
||||
if query_key == target_key:
|
||||
return index
|
||||
return None
|
||||
|
||||
def insert_tool(self, index, tool):
|
||||
def insert_tool(self, index: int, tool) -> None:
|
||||
key = f"tool_{tool.id}"
|
||||
self.insert(index, key, tool)
|
||||
|
||||
def get_tool_with_id(self, tool_id):
|
||||
def get_tool_with_id(self, tool_id: str):
|
||||
key = f"tool_{tool_id}"
|
||||
return self[key]
|
||||
|
||||
def append_tool(self, tool):
|
||||
def append_tool(self, tool) -> None:
|
||||
key = f"tool_{tool.id}"
|
||||
self[key] = tool
|
||||
|
||||
def stub_tool(self, key):
|
||||
def stub_tool(self, key: str) -> None:
|
||||
key = f"tool_{key}"
|
||||
self[key] = None
|
||||
|
||||
def stub_workflow(self, key):
|
||||
def stub_workflow(self, key: str) -> None:
|
||||
key = f'workflow_{key}'
|
||||
self[key] = None
|
||||
|
||||
def stub_label(self, key):
|
||||
def stub_label(self, key: str) -> None:
|
||||
key = f'label_{key}'
|
||||
self[key] = None
|
||||
|
||||
def append_section(self, key, section_elems):
|
||||
self[key] = section_elems
|
||||
def append_section(self, key: str, section: ToolSection) -> None:
|
||||
self[key] = section
|
||||
|
||||
def panel_items(self):
|
||||
return self
|
||||
|
||||
def walk_sections(self):
|
||||
for key, item in self.items():
|
||||
if isinstance(item, ToolSection):
|
||||
yield (key, item)
|
||||
|
||||
def closest_section(self, target_section_id: Optional[str], target_section_name: Optional[str]):
|
||||
for (section_id, section) in self.walk_sections():
|
||||
if section_id == target_section_id:
|
||||
return section
|
||||
|
||||
for (_, section) in self.walk_sections():
|
||||
if section.name == target_section_name:
|
||||
return section
|
||||
|
||||
return None
|
||||
|
||||
def apply_filter(self, f):
|
||||
to_remove = []
|
||||
|
||||
for key, item in self.items():
|
||||
if not f(key, item):
|
||||
to_remove.append(key)
|
||||
|
||||
for key in to_remove:
|
||||
del self[key]
|
||||
|
||||
def copy(self) -> 'ToolPanelElements':
|
||||
the_copy = ToolPanelElements()
|
||||
the_copy.update(self)
|
||||
return the_copy
|
||||
|
||||
def has_item_recursive(self, item):
|
||||
"""Check panel and section elements for supplied item."""
|
||||
for value in self.values():
|
||||
if value == item:
|
||||
return True
|
||||
if isinstance(value, ToolSection):
|
||||
if item in value.elems.values():
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -137,6 +137,12 @@ def ensure_tool_conf_item(xml_or_item):
|
||||
return None
|
||||
elif isinstance(xml_or_item, ToolConfItem):
|
||||
return xml_or_item
|
||||
elif isinstance(xml_or_item, dict):
|
||||
# TODO: handle sections...
|
||||
as_dict = xml_or_item.copy()
|
||||
type = as_dict.pop("type")
|
||||
attributes = as_dict
|
||||
return ToolConfItem(type, attributes, None)
|
||||
else:
|
||||
elem = xml_or_item
|
||||
type = elem.tag
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
class StaticToolBoxViewTypeEnum(str, Enum):
|
||||
generic = 'generic'
|
||||
activity = 'activity'
|
||||
publication = 'publication'
|
||||
training = 'training'
|
||||
|
||||
|
||||
class ExcludeTool(BaseModel):
|
||||
tool_id: str
|
||||
|
||||
|
||||
class ExcludeToolRegex(BaseModel):
|
||||
tool_id_regex: str
|
||||
|
||||
|
||||
class ExcludeTypes(BaseModel):
|
||||
types: List[str]
|
||||
|
||||
|
||||
Exclusions = Union[
|
||||
ExcludeTool,
|
||||
ExcludeToolRegex,
|
||||
ExcludeTypes,
|
||||
]
|
||||
OptionalExclusionList = Optional[List[Exclusions]]
|
||||
|
||||
|
||||
class Tool(BaseModel):
|
||||
content_type: Literal['tool'] = Field(alias="type")
|
||||
id: str
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class Label(BaseModel):
|
||||
content_type: Literal['label'] = Field(alias="type")
|
||||
id: Optional[str]
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class LabelShortcut(BaseModel):
|
||||
content_type = "simple_label"
|
||||
label: str
|
||||
|
||||
|
||||
class Workflow(BaseModel):
|
||||
content_type: Literal['workflow'] = Field(alias="type")
|
||||
id: str
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class ItemsFrom(BaseModel):
|
||||
content_type = "items_from"
|
||||
items_from: str
|
||||
excludes: OptionalExclusionList
|
||||
|
||||
|
||||
SectionContent = Union[
|
||||
Tool,
|
||||
Label,
|
||||
LabelShortcut,
|
||||
Workflow,
|
||||
ItemsFrom,
|
||||
]
|
||||
|
||||
|
||||
class HasItems:
|
||||
|
||||
@property
|
||||
def items_expanded(self):
|
||||
# replace SectionAliases with individual SectionAlias objects
|
||||
# replace LabelShortcuts with Labels
|
||||
items = []
|
||||
for item in self.items:
|
||||
if isinstance(item, SectionAliases):
|
||||
for section in item.sections:
|
||||
section_alias = SectionAlias(
|
||||
section=section,
|
||||
excludes=item.excludes,
|
||||
)
|
||||
items.append(section_alias)
|
||||
elif isinstance(item, LabelShortcut):
|
||||
new_item = Label(
|
||||
id=None,
|
||||
text=item.label,
|
||||
content_type="label",
|
||||
)
|
||||
items.append(new_item)
|
||||
else:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
class Section(BaseModel, HasItems):
|
||||
content_type: Literal['section'] = Field(alias="type")
|
||||
id: Optional[str]
|
||||
name: Optional[str]
|
||||
items: Optional[List[SectionContent]]
|
||||
excludes: OptionalExclusionList
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class SectionAlias(BaseModel):
|
||||
content_type = "section_alias"
|
||||
section: str
|
||||
excludes: OptionalExclusionList
|
||||
|
||||
|
||||
class SectionAliases(BaseModel):
|
||||
content_type = "section_aliases"
|
||||
sections: List[str]
|
||||
excludes: OptionalExclusionList
|
||||
|
||||
|
||||
RootContent = Union[
|
||||
Section,
|
||||
SectionAlias,
|
||||
SectionAliases,
|
||||
Tool,
|
||||
Label,
|
||||
LabelShortcut,
|
||||
Workflow,
|
||||
ItemsFrom,
|
||||
]
|
||||
|
||||
|
||||
class StaticToolBoxView(BaseModel, HasItems):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
view_type: StaticToolBoxViewTypeEnum = Field(alias="type")
|
||||
items: List[RootContent]
|
||||
excludes: Optional[List[Exclusions]]
|
||||
|
||||
@staticmethod
|
||||
def from_dict(as_dict):
|
||||
return StaticToolBoxView(**as_dict)
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
@@ -0,0 +1,168 @@
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from galaxy.tool_util.edam_util import (
|
||||
load_edam_tree,
|
||||
ROOT_OPERATION,
|
||||
ROOT_TOPIC,
|
||||
)
|
||||
from galaxy.util import (
|
||||
etree,
|
||||
ExecutionTimer,
|
||||
)
|
||||
from .interface import (
|
||||
ToolBoxRegistry,
|
||||
ToolPanelView,
|
||||
ToolPanelViewModel,
|
||||
ToolPanelViewModelType,
|
||||
walk_loaded_tools,
|
||||
)
|
||||
from ..panel import (
|
||||
ToolPanelElements,
|
||||
ToolSectionLabel,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdamPanelMode(str, Enum):
|
||||
merged = "merged"
|
||||
topics = "topics"
|
||||
operations = "operations"
|
||||
|
||||
|
||||
class EdamToolPanelView(ToolPanelView):
|
||||
|
||||
def __init__(self, edam_ontology_path: Optional[str], mode: EdamPanelMode = EdamPanelMode.merged):
|
||||
edam = load_edam_tree(None if not edam_ontology_path or not os.path.exists(edam_ontology_path) else edam_ontology_path)
|
||||
self.edam = edam
|
||||
self.mode = mode
|
||||
self.include_topics = mode in [EdamPanelMode.merged, EdamPanelMode.topics]
|
||||
self.include_operations = mode in [EdamPanelMode.merged, EdamPanelMode.operations]
|
||||
|
||||
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
|
||||
execution_timer = ExecutionTimer()
|
||||
|
||||
# Find the children of the top level topics
|
||||
operations_list = [ROOT_OPERATION] + list(self._edam_children_of(ROOT_OPERATION))
|
||||
topics_list = [ROOT_TOPIC] + list(self._edam_children_of(ROOT_TOPIC))
|
||||
|
||||
# Sort them (by english label)
|
||||
# operations = sorted(operations, key=lambda x: self.edam[x]['label'])
|
||||
# topics = sorted(topics, key=lambda x: self.edam[x]['label'])
|
||||
|
||||
# Convert these to list of dicts, wherein we'll add our tools/etc.
|
||||
operations: Dict[str, Dict] = {
|
||||
x: {}
|
||||
for x in operations_list
|
||||
}
|
||||
topics: Dict[str, Dict] = {
|
||||
x: {}
|
||||
for x in topics_list
|
||||
}
|
||||
uncategorized: List[Tuple] = []
|
||||
|
||||
for tool_id, key, val, val_name in walk_loaded_tools(base_tool_panel, toolbox_registry):
|
||||
for term in self._get_edam_sec(val):
|
||||
if term == 'uncategorized':
|
||||
uncategorized.append((tool_id, key, val, val_name))
|
||||
else:
|
||||
for path in self.edam[term]['path']:
|
||||
if len(path) == 1:
|
||||
t = term
|
||||
else:
|
||||
t = path[0]
|
||||
|
||||
if path[0].startswith('operation_'):
|
||||
operations[t][tool_id] = (term, tool_id, key, val, val_name)
|
||||
elif path[0].startswith('topic_'):
|
||||
topics[t][tool_id] = (term, tool_id, key, val, val_name)
|
||||
|
||||
new_panel = ToolPanelElements()
|
||||
for term in sorted(operations.keys(), key=lambda x: self._sort_edam_key(x)):
|
||||
if len(operations[term].keys()) == 0:
|
||||
continue
|
||||
|
||||
elem = etree.Element('label')
|
||||
elem.attrib['text'] = self.edam[term]['label']
|
||||
elem.attrib['id'] = term
|
||||
new_panel[f"label_{term}"] = ToolSectionLabel(elem)
|
||||
|
||||
for (term, tool_id, key, val, val_name) in operations[term].values():
|
||||
section = new_panel.get_or_create_section(term, self.edam[term]['label'])
|
||||
|
||||
toolbox_registry.add_tool_to_tool_panel_view(val, section)
|
||||
new_panel.record_section_for_tool_id(tool_id, key, val_name)
|
||||
|
||||
for term in sorted(topics.keys(), key=lambda x: self._sort_edam_key(x)):
|
||||
if len(topics[term].keys()) == 0:
|
||||
continue
|
||||
|
||||
elem = etree.Element('label')
|
||||
elem.attrib['text'] = self.edam[term]['label']
|
||||
elem.attrib['id'] = term
|
||||
new_panel[f"label_{term}"] = ToolSectionLabel(elem)
|
||||
|
||||
for (term, tool_id, key, val, val_name) in topics[term].values():
|
||||
section = new_panel.get_or_create_section(term, self.edam[term]['label'])
|
||||
toolbox_registry.add_tool_to_tool_panel_view(val, section)
|
||||
new_panel.record_section_for_tool_id(tool_id, key, val_name)
|
||||
|
||||
section = new_panel.get_or_create_section('uncategorized', 'Uncategorized')
|
||||
for (tool_id, key, val, val_name) in uncategorized:
|
||||
toolbox_registry.add_tool_to_tool_panel_view(val, section)
|
||||
new_panel.record_section_for_tool_id(tool_id, key, val_name)
|
||||
log.debug("Loading EDAM tool panel finished %s", execution_timer)
|
||||
return new_panel
|
||||
|
||||
def _get_edam_sec(self, tool):
|
||||
edam = []
|
||||
if self.include_operations:
|
||||
edam.extend(tool.edam_operations)
|
||||
if self.include_topics:
|
||||
edam.extend(tool.edam_topics)
|
||||
if len(edam) > 0:
|
||||
for term in edam:
|
||||
yield term
|
||||
else:
|
||||
yield 'uncategorized'
|
||||
|
||||
def _sort_edam_key(self, x):
|
||||
if x in (ROOT_OPERATION, ROOT_TOPIC):
|
||||
return f"!{x}"
|
||||
else:
|
||||
return self.edam[x]['label']
|
||||
|
||||
def _edam_children_of(self, parentTerm):
|
||||
for term in self.edam.keys():
|
||||
if parentTerm in self.edam[term]['parents']:
|
||||
yield term
|
||||
|
||||
def to_model(self) -> ToolPanelViewModel:
|
||||
mode = self.mode
|
||||
if mode == EdamPanelMode.merged:
|
||||
model_id = "ontology:edam_merged"
|
||||
name = "EDAM Operations and Topics"
|
||||
description = "Tools are grouped using both annotated operation and topic information (if availabled)."
|
||||
elif mode == EdamPanelMode.operations:
|
||||
model_id = "ontology:edam_operations"
|
||||
name = "EDAM Operations"
|
||||
description = "Tools are grouped using annotated EDAM operation information (if availabled)."
|
||||
elif mode == EdamPanelMode.topics:
|
||||
model_id = "ontology:edam_topics"
|
||||
name = "EDAM Topics"
|
||||
description = "Tools are grouped using annotated EDAM topic information (if availabled)."
|
||||
else:
|
||||
raise AssertionError(f"Invalid EDAM mode encountered {mode}")
|
||||
model_class = self.__class__.__name__
|
||||
view_type = ToolPanelViewModelType.ontology
|
||||
return ToolPanelViewModel(
|
||||
id=model_id,
|
||||
name=name,
|
||||
description=description,
|
||||
model_class=model_class,
|
||||
view_type=view_type,
|
||||
searchable=True,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..panel import (
|
||||
HasPanelItems,
|
||||
panel_item_types,
|
||||
ToolPanelElements,
|
||||
)
|
||||
|
||||
|
||||
class ToolPanelViewModelType(str, Enum):
|
||||
default_type = 'default'
|
||||
generic = 'generic'
|
||||
activity = 'activity'
|
||||
ontology = 'ontology'
|
||||
publication = 'publication'
|
||||
training = 'training'
|
||||
|
||||
|
||||
class ToolPanelViewModel(BaseModel):
|
||||
"""A view of ToolPanelView objects serialized for the API."""
|
||||
id: str
|
||||
model_class: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
view_type: ToolPanelViewModelType
|
||||
searchable: bool # Allow for more dynamic views that don't plug into fixed search indicies in the future...
|
||||
|
||||
|
||||
class ToolBoxRegistry:
|
||||
"""View of ToolBox provided to ToolPanelView to reason about tools loaded."""
|
||||
|
||||
@abstractmethod
|
||||
def has_tool(self, tool_id: str) -> bool:
|
||||
"""Return bool indicating if tool with specified id is loaded."""
|
||||
|
||||
@abstractmethod
|
||||
def get_tool(self, tool_id: str):
|
||||
"""Return tool with supplied tool id."""
|
||||
|
||||
@abstractmethod
|
||||
def get_workflow(self, id: str):
|
||||
"""Return workflow from panel with supplied id."""
|
||||
|
||||
@abstractmethod
|
||||
def add_tool_to_tool_panel_view(self, tool, tool_panel_component: HasPanelItems) -> None:
|
||||
"""Add tool to the tool panel view component (root or section)."""
|
||||
|
||||
|
||||
class ToolPanelView:
|
||||
|
||||
@abstractmethod
|
||||
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
|
||||
"""Consume tool panel state and return custom tool panel view."""
|
||||
|
||||
@abstractmethod
|
||||
def to_model(self) -> ToolPanelViewModel:
|
||||
"""Convert abstract description to dictionary description to emit via the API."""
|
||||
|
||||
|
||||
def walk_loaded_tools(tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry):
|
||||
for key, item_type, val in tool_panel.panel_items_iter():
|
||||
if item_type == panel_item_types.TOOL:
|
||||
tool_id = key.replace('tool_', '', 1)
|
||||
if toolbox_registry.has_tool(tool_id):
|
||||
yield (tool_id, key, val, val.name)
|
||||
elif item_type == panel_item_types.SECTION:
|
||||
for section_key, section_item_type, section_val in val.panel_items_iter():
|
||||
if section_item_type == panel_item_types.TOOL:
|
||||
tool_id = section_key.replace('tool_', '', 1)
|
||||
if toolbox_registry.has_tool(tool_id):
|
||||
yield (tool_id, key, section_val, val.name)
|
||||
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
import yaml
|
||||
|
||||
from galaxy.util import config_directories_from_setting
|
||||
from .definitions import StaticToolBoxView
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
EXTENSIONS = [".yml", ".yaml", ".json"]
|
||||
|
||||
|
||||
class StaticToolBoxViewSources:
|
||||
view_directories: List[str]
|
||||
view_dicts: List[Dict]
|
||||
|
||||
def __init__(self, view_directories=None, view_dicts=None):
|
||||
self.view_directories = config_directories_from_setting(view_directories) or []
|
||||
self.view_dicts = view_dicts or []
|
||||
|
||||
def get_definitions(self) -> List[StaticToolBoxView]:
|
||||
view_definitions = []
|
||||
|
||||
for view_dict in self.view_dicts:
|
||||
view_definitions.append(StaticToolBoxView.from_dict(view_dict))
|
||||
|
||||
for view_directory in self.view_directories:
|
||||
if not os.path.exists(view_directory):
|
||||
log.warning(f"Failed to find toolbox view directory {view_directory}")
|
||||
|
||||
for filename in os.listdir(view_directory):
|
||||
if not looks_like_view_source_filename(filename):
|
||||
continue
|
||||
|
||||
view_path = os.path.join(view_directory, filename)
|
||||
with open(view_path) as f:
|
||||
view_dict = yaml.safe_load(f)
|
||||
if "id" not in view_dict:
|
||||
file_id = os.path.splitext(filename)[0]
|
||||
view_dict["id"] = file_id
|
||||
view_definitions.append(StaticToolBoxView.from_dict(view_dict))
|
||||
|
||||
return view_definitions
|
||||
|
||||
|
||||
def looks_like_view_source_filename(filename: str) -> bool:
|
||||
for ext in EXTENSIONS:
|
||||
if filename.endswith(ext):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,168 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .definitions import (
|
||||
ExcludeTool,
|
||||
ExcludeToolRegex,
|
||||
ExcludeTypes,
|
||||
Section,
|
||||
StaticToolBoxView,
|
||||
Workflow,
|
||||
)
|
||||
from .interface import (
|
||||
ToolBoxRegistry,
|
||||
ToolPanelView,
|
||||
ToolPanelViewModel,
|
||||
ToolPanelViewModelType,
|
||||
)
|
||||
from ..panel import (
|
||||
ToolPanelElements,
|
||||
ToolSection,
|
||||
ToolSectionLabel,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_filter(excludes_):
|
||||
excludes = excludes_ or []
|
||||
|
||||
def filter_function(panel_key, panel_value):
|
||||
for exclude in excludes:
|
||||
if panel_key.startswith("tool_"):
|
||||
if isinstance(exclude, ExcludeTool):
|
||||
if panel_value.id == exclude.tool_id:
|
||||
return False
|
||||
if panel_value.old_id == exclude.tool_id:
|
||||
return False
|
||||
|
||||
if isinstance(exclude, ExcludeToolRegex):
|
||||
if re.match(exclude.tool_id_regex, panel_value.id):
|
||||
return False
|
||||
if re.match(exclude.tool_id_regex, panel_value.old_id):
|
||||
return False
|
||||
|
||||
if isinstance(exclude, ExcludeTypes):
|
||||
if panel_key.startswith("label_") and "label" in exclude.types:
|
||||
return False
|
||||
if panel_key.startswith("tool_") and "tool" in exclude.types:
|
||||
return False
|
||||
if panel_key.startswith("workflow_") and "workflow" in exclude.types:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return filter_function
|
||||
|
||||
|
||||
class StaticToolPanelView(ToolPanelView):
|
||||
_definition: StaticToolBoxView
|
||||
|
||||
def __init__(self, definition: StaticToolBoxView):
|
||||
self._definition = definition
|
||||
|
||||
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
|
||||
|
||||
def apply_filter(definition, elems):
|
||||
if definition.excludes:
|
||||
elems.apply_filter(build_filter(definition.excludes))
|
||||
|
||||
def definition_with_items_to_panel(definition, allow_sections: bool = True):
|
||||
new_panel = ToolPanelElements()
|
||||
for element in definition.items_expanded:
|
||||
if element.content_type == "section":
|
||||
assert allow_sections
|
||||
section_def: Section = element
|
||||
section: ToolSection
|
||||
assert section_def.id is not None or section_def.name is not None
|
||||
if element.items:
|
||||
panel = definition_with_items_to_panel(section_def, allow_sections=False)
|
||||
section = ToolSection()
|
||||
if section_def.name is not None:
|
||||
name = section_def.name
|
||||
else:
|
||||
assert section_def.id is not None
|
||||
name = section_def.id
|
||||
section.name = name
|
||||
if section_def.id is not None:
|
||||
section.id = section_def.id
|
||||
else:
|
||||
# TODO: there has to be tool shed code to do this in a consistent way... where is it?
|
||||
section.id = name.replace(" ", "-").lower()
|
||||
section.elems = panel
|
||||
else:
|
||||
closest_section = base_tool_panel.closest_section(section_def.id, section_def.name)
|
||||
if closest_section is None:
|
||||
log.warning(f"Failed to find matching section for (id, name) = ({section_def.id}, {section_def.name})")
|
||||
continue
|
||||
section = closest_section.copy()
|
||||
if section_def.id is not None:
|
||||
section.id = section_def.id
|
||||
if section_def.name is not None:
|
||||
section.name = section_def.name
|
||||
apply_filter(section_def, section.elems)
|
||||
new_panel.append_section(section.id, section)
|
||||
elif element.content_type == "section_alias":
|
||||
assert allow_sections
|
||||
closest_section = base_tool_panel.closest_section(element.section, element.section)
|
||||
if closest_section is None:
|
||||
log.warning(f"Failed to find matching section for (id, name) = ({element.section}, {element.section})")
|
||||
continue
|
||||
section = closest_section.copy()
|
||||
apply_filter(element, section.elems)
|
||||
new_panel.append_section(section.id, section)
|
||||
elif element.content_type == "label":
|
||||
as_dict = {
|
||||
"id": element.id or element.text.lower().replace(" ", "-"),
|
||||
"text": element.text,
|
||||
"type": "label",
|
||||
}
|
||||
label = ToolSectionLabel(as_dict)
|
||||
key = f"label_{label.id}"
|
||||
new_panel[key] = label
|
||||
elif element.content_type == "tool":
|
||||
tool_id = element.id
|
||||
if not toolbox_registry.has_tool(tool_id):
|
||||
log.warning(f"Failed to find tool_id {tool_id} from parent toolbox, cannot load into panel view")
|
||||
continue
|
||||
tool = toolbox_registry.get_tool(tool_id)
|
||||
toolbox_registry.add_tool_to_tool_panel_view(tool, new_panel)
|
||||
elif element.content_type == "workflow":
|
||||
workflow_def: Workflow = element
|
||||
workflow = toolbox_registry.get_workflow(element.id)
|
||||
panel_id = f"workflow_{workflow_def.id}"
|
||||
new_panel[panel_id] = workflow
|
||||
elif element.content_type == "items_from":
|
||||
closest_section = base_tool_panel.closest_section(element.items_from, element.items_from)
|
||||
if closest_section is None:
|
||||
log.warning(f"Failed to find matching section for (id, name) = ({element.items_from}, None)")
|
||||
continue
|
||||
elems = closest_section.elems.copy()
|
||||
apply_filter(element, elems)
|
||||
for key, item in elems.items():
|
||||
new_panel[key] = item
|
||||
else:
|
||||
raise AssertionError("Unknown static toolbox configuration element encountered.")
|
||||
|
||||
if definition.excludes:
|
||||
new_panel.apply_filter(build_filter(definition.excludes))
|
||||
|
||||
return new_panel
|
||||
|
||||
return definition_with_items_to_panel(self._definition)
|
||||
|
||||
def to_model(self) -> ToolPanelViewModel:
|
||||
model_id = self._definition.id
|
||||
name = self._definition.name
|
||||
description = self._definition.description
|
||||
view_type = ToolPanelViewModelType[self._definition.view_type.value]
|
||||
model_class = self.__class__.__name__
|
||||
return ToolPanelViewModel(
|
||||
id=model_id,
|
||||
name=name,
|
||||
description=description,
|
||||
model_class=model_class,
|
||||
view_type=view_type,
|
||||
searchable=True,
|
||||
)
|
||||
@@ -49,6 +49,7 @@ from galaxy.tool_util.parser import (
|
||||
from galaxy.tool_util.parser.xml import XmlPageSource
|
||||
from galaxy.tool_util.provided_metadata import parse_tool_provided_metadata
|
||||
from galaxy.tool_util.toolbox import BaseGalaxyToolBox
|
||||
from galaxy.tool_util.toolbox.views.sources import StaticToolBoxViewSources
|
||||
from galaxy.tools import expressions
|
||||
from galaxy.tools.actions import DefaultToolAction
|
||||
from galaxy.tools.actions.data_manager import DataManagerToolAction
|
||||
@@ -279,10 +280,17 @@ class ToolBox(BaseGalaxyToolBox):
|
||||
# FIXME: ./
|
||||
if tool_root_dir == './tools':
|
||||
tool_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bundled'))
|
||||
view_sources = StaticToolBoxViewSources(
|
||||
view_directories=app.config.panel_views_dir,
|
||||
view_dicts=app.config.panel_views,
|
||||
)
|
||||
default_panel_view = app.config.default_panel_view
|
||||
super().__init__(
|
||||
config_filenames=config_filenames,
|
||||
tool_root_dir=tool_root_dir,
|
||||
app=app,
|
||||
view_sources=view_sources,
|
||||
default_panel_view=default_panel_view,
|
||||
save_integrated_tool_panel=save_integrated_tool_panel,
|
||||
)
|
||||
|
||||
@@ -771,7 +779,7 @@ class Tool(Dictifiable):
|
||||
return self.app.job_config.get_destination(self.__get_job_tool_configuration(job_params=job_params).destination)
|
||||
|
||||
def get_panel_section(self):
|
||||
return self.app.toolbox.get_integrated_section_for_tool(self)
|
||||
return self.app.toolbox.get_section_for_tool(self)
|
||||
|
||||
def allow_user_access(self, user, attempting_access=True):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ through the library docs at https://whoosh.readthedocs.io.
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Tuple, Union
|
||||
|
||||
from whoosh import (
|
||||
analysis,
|
||||
@@ -34,8 +35,11 @@ from galaxy.web.framework.helpers import to_unicode
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CanConvertToFloat = Union[str, int, float]
|
||||
CanConvertToInt = Union[str, int, float]
|
||||
|
||||
def get_or_create_index(index_dir, schema):
|
||||
|
||||
def get_or_create_index(index_dir: str, schema: Schema) -> index.Index:
|
||||
if not os.path.exists(index_dir):
|
||||
os.makedirs(index_dir)
|
||||
if index.exists_in(index_dir):
|
||||
@@ -49,12 +53,44 @@ def get_or_create_index(index_dir, schema):
|
||||
|
||||
|
||||
class ToolBoxSearch:
|
||||
"""Support searching across all fixed panel views in a toolbox.
|
||||
|
||||
Search is delegated off to ToolPanelViewSearch for each panel object.
|
||||
"""
|
||||
|
||||
def __init__(self, toolbox, index_dir: str, index_help: bool = True):
|
||||
panel_searches = {}
|
||||
for panel_view in toolbox.panel_views():
|
||||
panel_view_id = panel_view.id
|
||||
panel_index_dir = os.path.join(index_dir, panel_view_id)
|
||||
panel_searches[panel_view_id] = ToolPanelViewSearch(toolbox, panel_view_id, panel_index_dir, index_help=index_help)
|
||||
self.panel_searches = panel_searches
|
||||
# We keep track of how many times the tool index has been rebuilt.
|
||||
# We start at -1, so that after the first index the count is at 0,
|
||||
# which is the same as the toolbox reload count. This way we can skip
|
||||
# reindexing if the index count is equal to the toolbox reload count.
|
||||
self.index_count = -1
|
||||
|
||||
def build_index(self, tool_cache, index_help: bool = True) -> None:
|
||||
self.index_count += 1
|
||||
for panel_search in self.panel_searches.values():
|
||||
panel_search.build_index(tool_cache, index_help=index_help)
|
||||
|
||||
def search(self, *args, **kwd) -> List[str]:
|
||||
panel_view = kwd.pop("panel_view")
|
||||
if panel_view not in self.panel_searches:
|
||||
raise KeyError(f"Unknown panel_view specified {panel_view}")
|
||||
panel_search = self.panel_searches[panel_view]
|
||||
return panel_search.search(*args, **kwd)
|
||||
|
||||
|
||||
class ToolPanelViewSearch:
|
||||
"""
|
||||
Support searching tools in a toolbox. This implementation uses
|
||||
the Whoosh search library.
|
||||
"""
|
||||
|
||||
def __init__(self, toolbox, index_dir=None, index_help=True):
|
||||
def __init__(self, toolbox, panel_view_id: str, index_dir: str, index_help: bool = True):
|
||||
self.schema = Schema(id=ID(stored=True, unique=True),
|
||||
old_id=ID,
|
||||
stub=KEYWORD,
|
||||
@@ -66,23 +102,18 @@ class ToolBoxSearch:
|
||||
self.rex = analysis.RegexTokenizer()
|
||||
self.index_dir = index_dir
|
||||
self.toolbox = toolbox
|
||||
self.panel_view_id = panel_view_id
|
||||
self.index = self._index_setup()
|
||||
# We keep track of how many times the tool index has been rebuilt.
|
||||
# We start at -1, so that after the first index the count is at 0,
|
||||
# which is the same as the toolbox reload count. This way we can skip
|
||||
# reindexing if the index count is equal to the toolbox reload count.
|
||||
self.index_count = -1
|
||||
|
||||
def _index_setup(self):
|
||||
def _index_setup(self) -> index.Index:
|
||||
return get_or_create_index(index_dir=self.index_dir, schema=self.schema)
|
||||
|
||||
def build_index(self, tool_cache, index_help=True):
|
||||
def build_index(self, tool_cache, index_help: bool = True) -> None:
|
||||
"""
|
||||
Prepare search index for tools loaded in toolbox.
|
||||
Use `tool_cache` to determine which tools need indexing and which tools should be expired.
|
||||
"""
|
||||
log.debug('Starting to build toolbox index.')
|
||||
self.index_count += 1
|
||||
log.debug(f"Starting to build toolbox index of panel {self.panel_view_id}.")
|
||||
execution_timer = ExecutionTimer()
|
||||
with self.index.reader() as reader:
|
||||
# Index ocasionally contains empty stored fields
|
||||
@@ -102,7 +133,7 @@ class ToolBoxSearch:
|
||||
writer.delete_by_term('id', tool_id)
|
||||
for tool_id in tool_cache._new_tool_ids - indexed_tool_ids:
|
||||
tool = self.toolbox.get_tool(tool_id)
|
||||
if tool and tool.is_latest_version:
|
||||
if tool and tool.is_latest_version and self.toolbox.panel_has_tool(tool, self.panel_view_id):
|
||||
if tool.hidden:
|
||||
# we check if there is an older tool we can return
|
||||
if tool.lineage:
|
||||
@@ -117,9 +148,9 @@ class ToolBoxSearch:
|
||||
continue
|
||||
add_doc_kwds = self._create_doc(tool_id=tool_id, tool=tool, index_help=index_help)
|
||||
writer.update_document(**add_doc_kwds)
|
||||
log.debug("Toolbox index finished %s", execution_timer)
|
||||
log.debug(f"Toolbox index of panel {self.panel_view_id} finished {execution_timer}")
|
||||
|
||||
def _create_doc(self, tool_id, tool, index_help=True):
|
||||
def _create_doc(self, tool_id: str, tool, index_help: bool = True) -> Dict[str, str]:
|
||||
# Do not add data managers to the public index
|
||||
if tool.tool_type == 'manage_data':
|
||||
return {}
|
||||
@@ -153,10 +184,20 @@ class ToolBoxSearch:
|
||||
pass
|
||||
return add_doc_kwds
|
||||
|
||||
def search(self, q, tool_name_boost, tool_id_boost, tool_section_boost,
|
||||
tool_description_boost, tool_label_boost, tool_stub_boost,
|
||||
tool_help_boost, tool_search_limit, tool_enable_ngram_search,
|
||||
tool_ngram_minsize, tool_ngram_maxsize):
|
||||
def search(
|
||||
self, q: str,
|
||||
tool_name_boost: CanConvertToFloat,
|
||||
tool_id_boost: CanConvertToFloat,
|
||||
tool_section_boost: CanConvertToFloat,
|
||||
tool_description_boost: CanConvertToFloat,
|
||||
tool_label_boost: CanConvertToFloat,
|
||||
tool_stub_boost: CanConvertToFloat,
|
||||
tool_help_boost: CanConvertToFloat,
|
||||
tool_search_limit: CanConvertToFloat,
|
||||
tool_enable_ngram_search: bool,
|
||||
tool_ngram_minsize: CanConvertToInt,
|
||||
tool_ngram_maxsize: CanConvertToInt
|
||||
) -> List[str]:
|
||||
"""
|
||||
Perform search on the in-memory index. Weight in the given boosts.
|
||||
"""
|
||||
@@ -194,13 +235,13 @@ class ToolBoxSearch:
|
||||
hits = self.searcher.search(parsed_query, limit=float(tool_search_limit), sortedby='')
|
||||
return [hit['id'] for hit in hits]
|
||||
|
||||
def _search_ngrams(self, cleaned_query, tool_ngram_minsize, tool_ngram_maxsize, tool_search_limit):
|
||||
def _search_ngrams(self, cleaned_query: str, tool_ngram_minsize: CanConvertToInt, tool_ngram_maxsize: CanConvertToInt, tool_search_limit: CanConvertToFloat) -> List[str]:
|
||||
"""
|
||||
Break tokens into ngrams and search on those instead.
|
||||
This should make searching more resistant to typos and unfinished words.
|
||||
See docs at https://whoosh.readthedocs.io/en/latest/ngrams.html
|
||||
"""
|
||||
hits_with_score = {}
|
||||
hits_with_score: Dict[str, float] = {}
|
||||
token_analyzer = StandardAnalyzer() | analysis.NgramFilter(minsize=int(tool_ngram_minsize), maxsize=int(tool_ngram_maxsize))
|
||||
ngrams = [token.text for token in token_analyzer(cleaned_query)]
|
||||
for query in ngrams:
|
||||
@@ -219,6 +260,6 @@ class ToolBoxSearch:
|
||||
if not is_present:
|
||||
hits_with_score[curr_hit['id']] = curr_hits.score(i)
|
||||
# Sort the results based on aggregated BM25 score in decreasing order of scores
|
||||
hits_with_score = sorted(hits_with_score.items(), key=lambda x: x[1], reverse=True)
|
||||
hits_with_score_list: List[Tuple[str, float]] = sorted(hits_with_score.items(), key=lambda x: x[1], reverse=True)
|
||||
# Return the tool ids
|
||||
return [item[0] for item in hits_with_score[0:int(tool_search_limit)]]
|
||||
return [item[0] for item in hits_with_score_list[0:int(tool_search_limit)]]
|
||||
|
||||
@@ -307,7 +307,6 @@ class JSAppLauncher(BaseUIController):
|
||||
config = {
|
||||
'active_view': 'analysis',
|
||||
'enable_webhooks': True if trans.app.webhooks_registry.webhooks else False,
|
||||
'toolbox': trans.app.toolbox.to_dict(trans),
|
||||
'message_box_visible': trans.app.config.message_box_visible,
|
||||
'show_inactivity_warning': trans.app.config.user_activation_on and trans.user and not trans.user.active,
|
||||
'tool_shed_urls': list(trans.app.tool_shed_registry.tool_sheds.values()) if trans.app.tool_shed_registry else [],
|
||||
|
||||
@@ -46,6 +46,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
|
||||
:param in_panel: if true, tools are returned in panel structure,
|
||||
including sections and labels
|
||||
:param view: ToolBox view to apply (default is 'default')
|
||||
:param trackster: if true, only tools that are compatible with
|
||||
Trackster are returned
|
||||
:param q: if present search on the given query will be performed
|
||||
@@ -59,6 +60,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
q = kwds.get('q', '')
|
||||
tool_id = kwds.get('tool_id', '')
|
||||
tool_help = util.string_as_bool(kwds.get('tool_help', 'False'))
|
||||
view = kwds.get("view", None)
|
||||
|
||||
# Find whether to search.
|
||||
if q:
|
||||
@@ -69,7 +71,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
else:
|
||||
hits = None
|
||||
else:
|
||||
hits = self._search(q)
|
||||
hits = self._search(q, view)
|
||||
results = []
|
||||
if hits:
|
||||
for hit in hits:
|
||||
@@ -90,7 +92,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
|
||||
# Return everything.
|
||||
try:
|
||||
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help)
|
||||
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help, view=view)
|
||||
except Exception:
|
||||
raise exceptions.InternalServerError("Error: Could not convert toolbox to dictionary")
|
||||
|
||||
@@ -377,7 +379,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
detected_versions.append(tool.version)
|
||||
return detected_versions
|
||||
|
||||
def _search(self, q):
|
||||
def _search(self, q, view):
|
||||
"""
|
||||
Perform the search on the given query.
|
||||
Boosts and numer of results are configurable in galaxy.ini file.
|
||||
@@ -388,6 +390,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
:return: Dictionary containing the tools' ids of the best hits.
|
||||
:return type: dict
|
||||
"""
|
||||
panel_view = view or self.app.config.default_panel_view
|
||||
tool_name_boost = self.app.config.get('tool_name_boost', 9)
|
||||
tool_id_boost = self.app.config.get('tool_id_boost', 9)
|
||||
tool_section_boost = self.app.config.get('tool_section_boost', 3)
|
||||
@@ -401,6 +404,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
|
||||
tool_ngram_maxsize = self.app.config.get('tool_ngram_maxsize', 4)
|
||||
|
||||
results = self.app.toolbox_search.search(q=q,
|
||||
panel_view=panel_view,
|
||||
tool_name_boost=tool_name_boost,
|
||||
tool_id_boost=tool_id_boost,
|
||||
tool_section_boost=tool_section_boost,
|
||||
|
||||
@@ -2454,21 +2454,50 @@ mapping:
|
||||
stable API. (The module state definitions may change and workflows built using
|
||||
these modules may not function in the future.)
|
||||
|
||||
enable_beta_edam_toolbox:
|
||||
type: bool
|
||||
default: false
|
||||
edam_panel_views:
|
||||
type: str
|
||||
default: 'operations,topics'
|
||||
required: false
|
||||
desc: |
|
||||
Enable beta EDAM organised toolbox which ignores admin managed
|
||||
sections in lieu of EDAM topics only.
|
||||
Comma-separated list of the EDAM panel views to load - choose from merged, operations, topics.
|
||||
Set to empty string to disable EDAM all together. Set default_panel_view to 'ontology:edam_topics'
|
||||
to override default tool panel to use an EDAM view.
|
||||
|
||||
beta_edam_toolbox_ontology_path:
|
||||
edam_toolbox_ontology_path:
|
||||
type: str
|
||||
default: EDAM.tsv
|
||||
path_resolves_to: data_dir
|
||||
required: false
|
||||
desc: |
|
||||
Sets the path to EDAM ontology file.
|
||||
Sets the path to EDAM ontology file - if the path doesn't exist PyPI package data will be loaded.
|
||||
|
||||
panel_views_dir:
|
||||
type: str
|
||||
default: config/plugins/activities
|
||||
required: false
|
||||
desc: |
|
||||
Directory to check out for toolbox tool panel views. The path is relative to the
|
||||
Galaxy root dir. To use an absolute path begin the path with '/'. This is a
|
||||
comma-separated list.
|
||||
|
||||
panel_views:
|
||||
type: seq
|
||||
sequence:
|
||||
- type: any
|
||||
required: false
|
||||
desc: |
|
||||
Definitions of static toolbox panel views embedded directly in the config instead of reading
|
||||
YAML from directory with panel_views_dir.
|
||||
|
||||
default_panel_view:
|
||||
type: str
|
||||
default: default
|
||||
required: false
|
||||
desc: |
|
||||
Default tool panel view for the current Galaxy configuration. This should refer to an id of
|
||||
a panel view defined using the panel_views or panel_views_dir configuration options or an
|
||||
EDAM panel view. The default panel view is simply called `default` and refers to the tool
|
||||
panel state defined by the integrated tool panel.
|
||||
|
||||
default_workflow_export_format:
|
||||
type: str
|
||||
|
||||
@@ -724,7 +724,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
|
||||
'tags': item_tag_names,
|
||||
'initialVersion': version,
|
||||
'annotation': self.get_item_annotation_str(trans.sa_session, trans.user, stored),
|
||||
'toolbox': trans.app.toolbox.to_dict(trans),
|
||||
'moduleSections': module_sections,
|
||||
'dataManagers': data_managers,
|
||||
'workflows': workflows
|
||||
|
||||
@@ -15,6 +15,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import nose.config
|
||||
@@ -155,6 +156,7 @@ def setup_galaxy_config(
|
||||
conda_auto_init=False,
|
||||
conda_auto_install=False,
|
||||
use_shared_connection_for_amqp=False,
|
||||
allow_tool_conf_override: bool = True,
|
||||
):
|
||||
"""Setup environment and build config for test Galaxy instance."""
|
||||
# For certain docker operations this needs to be evaluated out - e.g. for cwltool.
|
||||
@@ -165,6 +167,7 @@ def setup_galaxy_config(
|
||||
new_file_path = tempfile.mkdtemp(prefix='new_files_path_', dir=tmpdir)
|
||||
job_working_directory = tempfile.mkdtemp(prefix='job_working_directory_', dir=tmpdir)
|
||||
|
||||
user_library_import_dir: Optional[str]
|
||||
if use_test_file_dir:
|
||||
first_test_file_dir = ensure_test_file_dir_set()
|
||||
if not os.path.isabs(first_test_file_dir):
|
||||
@@ -196,7 +199,10 @@ def setup_galaxy_config(
|
||||
# For storing Data Manager outputs and .loc files so that real ones don't get clobbered
|
||||
galaxy_data_manager_data_path = tempfile.mkdtemp(prefix='data_manager_tool-data', dir=tmpdir)
|
||||
|
||||
tool_conf = os.environ.get('GALAXY_TEST_TOOL_CONF', default_tool_conf)
|
||||
if allow_tool_conf_override:
|
||||
tool_conf = os.environ.get('GALAXY_TEST_TOOL_CONF', default_tool_conf)
|
||||
else:
|
||||
tool_conf = default_tool_conf
|
||||
conda_auto_install = os.environ.get('GALAXY_TEST_CONDA_AUTO_INSTALL', conda_auto_install)
|
||||
conda_auto_init = os.environ.get('GALAXY_TEST_CONDA_AUTO_INIT', conda_auto_init)
|
||||
conda_prefix = os.environ.get('GALAXY_TEST_CONDA_PREFIX')
|
||||
@@ -1002,7 +1008,8 @@ class GalaxyTestDriver(TestDriver):
|
||||
else:
|
||||
default_tool_conf = getattr(config_object, "default_tool_conf", None)
|
||||
datatypes_conf_override = getattr(config_object, "datatypes_conf_override", None)
|
||||
|
||||
allow_tool_conf_override = getattr(config_object, "allow_tool_conf_override", True)
|
||||
self.allow_tool_conf_override = allow_tool_conf_override
|
||||
self.default_tool_conf = default_tool_conf
|
||||
self.datatypes_conf_override = datatypes_conf_override
|
||||
|
||||
@@ -1047,7 +1054,8 @@ class GalaxyTestDriver(TestDriver):
|
||||
log_format=self.log_format,
|
||||
conda_auto_init=getattr(config_object, "conda_auto_init", False),
|
||||
conda_auto_install=getattr(config_object, "conda_auto_install", False),
|
||||
use_shared_connection_for_amqp=getattr(config_object, "use_shared_connection_for_amqp", False)
|
||||
use_shared_connection_for_amqp=getattr(config_object, "use_shared_connection_for_amqp", False),
|
||||
allow_tool_conf_override=self.allow_tool_conf_override,
|
||||
)
|
||||
galaxy_config = setup_galaxy_config(
|
||||
galaxy_db_path,
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ for ((i=0; i<${#PACKAGE_DIRS[@]}; i++)); do
|
||||
pip install -e '.[template,jstree]'
|
||||
fi
|
||||
if [ "$package_dir" = "tool_util" ]; then
|
||||
pip install -e '.[mulled]'
|
||||
pip install -e '.[mulled,edam]'
|
||||
fi
|
||||
|
||||
if [[ "$run_tests" == "1" ]]; then
|
||||
|
||||
@@ -2,3 +2,5 @@ galaxy-util>=20.1.0.dev0
|
||||
galaxy-containers
|
||||
lxml
|
||||
sortedcontainers
|
||||
pydantic
|
||||
typing-extensions
|
||||
|
||||
@@ -43,6 +43,7 @@ PACKAGES = [
|
||||
'galaxy.tool_util.toolbox',
|
||||
'galaxy.tool_util.toolbox.filters',
|
||||
'galaxy.tool_util.toolbox.lineages',
|
||||
'galaxy.tool_util.toolbox.views',
|
||||
'galaxy.tool_util.verify',
|
||||
'galaxy.tool_util.verify.asserts',
|
||||
]
|
||||
@@ -104,6 +105,9 @@ setup(
|
||||
'jinja2',
|
||||
'Whoosh',
|
||||
],
|
||||
'edam': [
|
||||
'edam-ontology',
|
||||
],
|
||||
},
|
||||
license="AFL",
|
||||
zip_safe=False,
|
||||
|
||||
@@ -34,6 +34,7 @@ circus = "*"
|
||||
cwltool = "==3.0.20201109103151"
|
||||
dictobj = "*"
|
||||
docutils = "!=0.17, !=0.17.1"
|
||||
edam-ontology = "*"
|
||||
Fabric3 = "*"
|
||||
fastapi = ">=0.65.2"
|
||||
fastapi-utils = "*"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<tool id="mapper" name="Mapper" version="0.1.0">
|
||||
<edam_topics>
|
||||
<edam_topic>topic_0102</edam_topic><!-- CompBio -> SeqAnalysis -> Mapping -->
|
||||
</edam_topics>
|
||||
<edam_operations>
|
||||
<edam_operation>operation_3198</edam_operation> <!-- Analysis -> SeqAnalysis -> Nucleic acid sequence analysis -> Sequence read processing -> Read Mapping -->
|
||||
</edam_operations>
|
||||
<command><![CDATA[
|
||||
cp '$__tool_directory__/1.bam' '$out_file1'
|
||||
]]></command>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
id: my-custom
|
||||
name: Custom Panel Filter
|
||||
type: generic
|
||||
items:
|
||||
- type: label
|
||||
text: The Start
|
||||
- type: tool
|
||||
id: empty_list
|
||||
- type: label
|
||||
text: The Middle
|
||||
- type: tool
|
||||
id: count_list
|
||||
- type: label
|
||||
text: The End
|
||||
@@ -0,0 +1,4 @@
|
||||
name: Filtered Test Section
|
||||
type: generic
|
||||
items:
|
||||
- sections: [test,filter]
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Filtered Test Section
|
||||
type: activity
|
||||
items:
|
||||
- type: section
|
||||
id: test
|
||||
excludes:
|
||||
- tool_id: multi_data_optional
|
||||
@@ -0,0 +1,8 @@
|
||||
name: Filtered Test Section
|
||||
type: activity
|
||||
items:
|
||||
- type: section
|
||||
id: test
|
||||
excludes:
|
||||
- tool_id_regex: 'multi_data_.*'
|
||||
- tool_id_regex: '.*_text_option'
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Custom Panel 4
|
||||
type: generic
|
||||
items:
|
||||
- type: label
|
||||
text: The Start
|
||||
- type: tool
|
||||
id: empty_list
|
||||
- type: label
|
||||
text: The Middle
|
||||
- type: tool
|
||||
id: count_list
|
||||
- type: label
|
||||
text: The End
|
||||
excludes:
|
||||
- types: [label]
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Merged Section Example
|
||||
type: generic
|
||||
items:
|
||||
- type: section
|
||||
id: my-new-section
|
||||
name: My New Section
|
||||
items:
|
||||
- items_from: test
|
||||
- items_from: filter
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Custom Panel in a New Section
|
||||
type: generic
|
||||
items:
|
||||
- type: section
|
||||
name: My Completely New Sectin
|
||||
items:
|
||||
- type: label
|
||||
text: The Start
|
||||
- type: tool
|
||||
id: empty_list
|
||||
- label: The Middle
|
||||
- type: tool
|
||||
id: count_list
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Merged Section Example
|
||||
type: generic
|
||||
items:
|
||||
- type: section
|
||||
id: my-new-section
|
||||
name: My New Section
|
||||
items:
|
||||
- items_from: test
|
||||
excludes:
|
||||
- tool_id_regex: '.*column.*'
|
||||
- type: label
|
||||
text: A cool label is here
|
||||
- items_from: filter
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Merged Section Example
|
||||
type: generic
|
||||
items:
|
||||
- type: section
|
||||
id: my-new-section
|
||||
name: My New Section
|
||||
items:
|
||||
- items_from: Test Section
|
||||
- items_from: filter
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Filtered Test Section
|
||||
type: activity
|
||||
items:
|
||||
- section: test
|
||||
excludes:
|
||||
- tool_id_regex: 'multi_data_.*'
|
||||
- tool_id_regex: '.*_text_option'
|
||||
@@ -0,0 +1,5 @@
|
||||
name: Filtering Tools
|
||||
type: activity
|
||||
items:
|
||||
- type: section
|
||||
id: filter
|
||||
@@ -0,0 +1,60 @@
|
||||
from galaxy_test.driver import integration_util
|
||||
|
||||
|
||||
class EdamToolboxIntegrationTestCase(integration_util.IntegrationTestCase):
|
||||
|
||||
framework_tool_and_types = True
|
||||
|
||||
@classmethod
|
||||
def handle_galaxy_config_kwds(cls, config):
|
||||
config["edam_panel_views"] = "merged"
|
||||
|
||||
def test_edam_toolbox(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="ontology:edam_merged"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
section_names = [s["name"] for s in sections]
|
||||
assert "Mapping" in section_names
|
||||
mapping_section = [s for s in sections if s["name"] == "Mapping"][0]
|
||||
mapping_section_elems = mapping_section["elems"]
|
||||
# make sure our mapper tool was mapped using Edam correctly...
|
||||
assert [x for x in mapping_section_elems if x["id"] == "mapper"]
|
||||
|
||||
config_index = self.galaxy_interactor.get("configuration")
|
||||
config_index.raise_for_status()
|
||||
panel_views = config_index.json()["panel_views"]
|
||||
assert len(panel_views) > 1
|
||||
assert isinstance(panel_views, dict)
|
||||
edam_panel_view = panel_views["ontology:edam_merged"]
|
||||
assert edam_panel_view["view_type"] == "ontology"
|
||||
|
||||
|
||||
class EdamToolboxDefaultIntegrationTestCase(integration_util.IntegrationTestCase):
|
||||
|
||||
framework_tool_and_types = True
|
||||
|
||||
@classmethod
|
||||
def handle_galaxy_config_kwds(cls, config):
|
||||
config["edam_panel_views"] = "topics"
|
||||
config["default_panel_view"] = "ontology:edam_topics"
|
||||
|
||||
def test_edam_toolbox(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
section_names = [s["name"] for s in sections]
|
||||
assert "Mapping" in section_names
|
||||
mapping_section = [s for s in sections if s["name"] == "Mapping"][0]
|
||||
mapping_section_elems = mapping_section["elems"]
|
||||
# make sure our mapper tool was mapped using Edam correctly...
|
||||
assert [x for x in mapping_section_elems if x["id"] == "mapper"]
|
||||
|
||||
config_index = self.galaxy_interactor.get("configuration")
|
||||
config_index.raise_for_status()
|
||||
panel_views = config_index.json()["panel_views"]
|
||||
assert len(panel_views) > 1
|
||||
assert isinstance(panel_views, dict)
|
||||
edam_panel_view = panel_views["ontology:edam_topics"]
|
||||
assert edam_panel_view["view_type"] == "ontology"
|
||||
@@ -0,0 +1,178 @@
|
||||
import os
|
||||
|
||||
from galaxy_test.driver import integration_util
|
||||
|
||||
THIS_DIR = os.path.dirname(__file__)
|
||||
PANEL_VIEWS_DIR_1 = os.path.join(THIS_DIR, "panel_views_1")
|
||||
|
||||
|
||||
class PanelViewsFromDirectoryIntegrationTestCase(integration_util.IntegrationTestCase):
|
||||
|
||||
framework_tool_and_types = True
|
||||
allow_tool_conf_override = False
|
||||
|
||||
@classmethod
|
||||
def handle_galaxy_config_kwds(cls, config):
|
||||
config["panel_views_dir"] = PANEL_VIEWS_DIR_1
|
||||
|
||||
def test_section_copy(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="filter"))
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
section_names = [s["name"] for s in sections]
|
||||
assert len(section_names) == 1
|
||||
assert "For Tours" in section_names
|
||||
|
||||
def test_custom_label_order(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="my-custom"))
|
||||
verify_my_custom(index)
|
||||
|
||||
def test_filtering_sections_by_tool_id(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_2"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
assert len(sections) == 1
|
||||
section = sections[0]
|
||||
tools = section["elems"]
|
||||
assert len(tools) == 3, len(tools)
|
||||
|
||||
def test_filtering_sections_by_tool_id_regex(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_3"))
|
||||
verify_custom_regex_filtered(index)
|
||||
|
||||
def test_filtering_root_by_type(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_4"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
assert len(index_as_list) == 2
|
||||
# Labels are filtered out...
|
||||
assert model_classes(index_as_list) == ["Tool", "Tool"]
|
||||
assert element_ids(index_as_list) == ["empty_list", "count_list"]
|
||||
|
||||
def test_custom_section_def(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_6"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
assert len(index_as_list) == 1
|
||||
assert model_classes(index_as_list) == ["ToolSection"]
|
||||
section = index_as_list[0]
|
||||
section_elems = section["elems"]
|
||||
assert len(section_elems) == 4, model_classes(section_elems)
|
||||
assert model_classes(section_elems) == ["ToolSectionLabel", "Tool", "ToolSectionLabel", "Tool"]
|
||||
assert element_ids(section_elems) == ["the-start", "empty_list", "the-middle", "count_list"]
|
||||
|
||||
def test_section_embed(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_5"))
|
||||
verify_custom_embed(index)
|
||||
|
||||
def test_section_embed_filtering(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_7"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
assert len(index_as_list) == 1
|
||||
assert model_classes(index_as_list) == ["ToolSection"]
|
||||
section = index_as_list[0]
|
||||
section_elems = section["elems"]
|
||||
assert len(section_elems) == 5, model_classes(section_elems)
|
||||
assert model_classes(section_elems) == ["Tool", "Tool", "Tool", "ToolSectionLabel", "Tool"]
|
||||
elem_ids = element_ids(section_elems)
|
||||
assert elem_ids[0:3] == ["multi_data_optional", "paths_as_file", "param_text_option"]
|
||||
assert elem_ids[4] == "Filter1"
|
||||
|
||||
def test_section_reference_by_name(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_8"))
|
||||
verify_custom_embed(index)
|
||||
|
||||
def test_section_alias(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_9"))
|
||||
verify_custom_regex_filtered(index)
|
||||
|
||||
def test_expand_section_aliases(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_10"))
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
assert len(index_as_list) == 2
|
||||
assert model_classes(index_as_list) == ["ToolSection", "ToolSection"]
|
||||
|
||||
|
||||
class PanelViewsFromConfigIntegrationTestCase(integration_util.IntegrationTestCase):
|
||||
|
||||
framework_tool_and_types = True
|
||||
|
||||
@classmethod
|
||||
def handle_galaxy_config_kwds(cls, config):
|
||||
config["panel_views"] = [
|
||||
{
|
||||
"id": "my-custom",
|
||||
"name": "My Custom",
|
||||
"type": "generic",
|
||||
"items": [
|
||||
{
|
||||
"type": "label",
|
||||
"text": "The Start",
|
||||
}, {
|
||||
"type": "tool",
|
||||
"id": "empty_list",
|
||||
}, {
|
||||
"type": "label",
|
||||
"text": "The Middle",
|
||||
}, {
|
||||
"type": "tool",
|
||||
"id": "count_list",
|
||||
}, {
|
||||
"type": "label",
|
||||
"text": "The End",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
config["default_panel_view"] = "my-custom"
|
||||
|
||||
def test_custom_label_order(self):
|
||||
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True))
|
||||
verify_my_custom(index)
|
||||
|
||||
|
||||
def verify_my_custom(index):
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
assert len(sections) == 0
|
||||
|
||||
assert len(index_as_list) == 5
|
||||
assert model_classes(index_as_list) == ["ToolSectionLabel", "Tool", "ToolSectionLabel", "Tool", "ToolSectionLabel"]
|
||||
|
||||
|
||||
def verify_custom_embed(index):
|
||||
# custom_5 / custom_8
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
assert len(index_as_list) == 1
|
||||
assert model_classes(index_as_list) == ["ToolSection"]
|
||||
section = index_as_list[0]
|
||||
assert section["name"] == "My New Section"
|
||||
assert section["id"] == "my-new-section"
|
||||
section_elems = section["elems"]
|
||||
assert len(section_elems) == 5, model_classes(section_elems)
|
||||
assert model_classes(section_elems) == ["Tool", "Tool", "Tool", "Tool", "Tool"]
|
||||
assert element_ids(section_elems) == ["multi_data_optional", "paths_as_file", "param_text_option", "column_param", "Filter1"]
|
||||
|
||||
|
||||
def verify_custom_regex_filtered(index):
|
||||
# custom_3 / custom_9
|
||||
index.raise_for_status()
|
||||
index_as_list = index.json()
|
||||
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
|
||||
assert len(sections) == 1
|
||||
section = sections[0]
|
||||
tools = section["elems"]
|
||||
assert len(tools) == 2, len(tools)
|
||||
|
||||
|
||||
def element_ids(elements):
|
||||
return [x["id"] for x in elements]
|
||||
|
||||
|
||||
def model_classes(elements):
|
||||
return [x["model_class"] for x in elements]
|
||||
@@ -99,7 +99,6 @@ class ExpectedValues:
|
||||
self._expected_paths = {
|
||||
'admin_tool_recommendations_path': self._in_config_dir('tool_recommendations_overwrite.yml'),
|
||||
'auth_config_file': self._in_config_dir('auth_conf.xml'),
|
||||
'beta_edam_toolbox_ontology_path': self._in_data_dir('EDAM.tsv'),
|
||||
'build_sites_config_file': self._in_sample_dir('build_sites.yml.sample'),
|
||||
'builds_file_path': self._in_root_dir('tool-data/shared/ucsc/builds.txt'),
|
||||
'cache_dir': self._in_data_dir('cache'),
|
||||
@@ -113,6 +112,7 @@ class ExpectedValues:
|
||||
'datatypes_config_file': self._in_sample_dir('datatypes_conf.xml.sample'),
|
||||
'dependency_resolvers_config_file': self._in_config_dir('dependency_resolvers_conf.xml'),
|
||||
'dynamic_proxy_session_map': self._in_data_dir('session_map.sqlite'),
|
||||
'edam_toolbox_ontology_path': self._in_data_dir('EDAM.tsv'),
|
||||
'error_report_file': self._in_config_dir('error_report.yml'),
|
||||
'file_path': self._in_data_dir('objects'),
|
||||
'file_sources_config_file': self._in_config_dir('file_sources_conf.yml'),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import tempfile
|
||||
from typing import Any, Dict
|
||||
|
||||
from edam_ontology.streams import tabular_stream
|
||||
|
||||
from galaxy.tool_util.edam_util import load_edam_tree
|
||||
|
||||
|
||||
def test_load_edam_tree():
|
||||
tree = load_edam_tree()
|
||||
_verify_tree(tree)
|
||||
|
||||
|
||||
def test_load_edam_tree_from_path():
|
||||
tf = tempfile.NamedTemporaryFile("w")
|
||||
tf.write(tabular_stream().read())
|
||||
tf.flush()
|
||||
tree = load_edam_tree(tf.name)
|
||||
_verify_tree(tree)
|
||||
|
||||
|
||||
def _verify_tree(tree: Dict[str, Any]):
|
||||
assert tree is not None
|
||||
assert "operation_0004" in tree
|
||||
assert "topic_3974" in tree
|
||||
assert tree["topic_3974"]["label"] == "Epistasis"
|
||||
assert tree["topic_3974"]["parents"] == ['topic_0622', 'topic_3295']
|
||||
assert tree["topic_3974"]["path"] == [['topic_3391', 'topic_0003'], ['topic_3070', 'topic_0003']]
|
||||
@@ -0,0 +1,101 @@
|
||||
import pydantic
|
||||
|
||||
from galaxy.tool_util.toolbox.views.definitions import StaticToolBoxView, StaticToolBoxViewTypeEnum
|
||||
|
||||
EXAMPLE_1 = {
|
||||
"id": "rna",
|
||||
"name": "RNA Seq",
|
||||
"type": "activity",
|
||||
"items": [
|
||||
{
|
||||
"type": "workflow",
|
||||
"id": "12345abcd",
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"name": "My Cool Section",
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"text": "My Cool Label",
|
||||
"id": "my-cool-label-id",
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"id": "cat1",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_root_parsing():
|
||||
view = StaticToolBoxView.from_dict(EXAMPLE_1)
|
||||
assert view.id == "rna"
|
||||
assert view.name == "RNA Seq"
|
||||
assert view.view_type == StaticToolBoxViewTypeEnum.activity
|
||||
assert len(view.items) == 4
|
||||
workflow_item = view.items[0]
|
||||
assert workflow_item.content_type == "workflow"
|
||||
assert workflow_item.id == "12345abcd"
|
||||
|
||||
section_item = view.items[1]
|
||||
assert section_item.content_type == "section"
|
||||
assert section_item.id is None
|
||||
assert section_item.name == "My Cool Section"
|
||||
|
||||
outer_label_item = view.items[2]
|
||||
assert outer_label_item.content_type == "label"
|
||||
assert outer_label_item.id == "my-cool-label-id"
|
||||
assert outer_label_item.text == "My Cool Label"
|
||||
|
||||
output_tool_item = view.items[3]
|
||||
assert output_tool_item.content_type == "tool"
|
||||
assert output_tool_item.id == "cat1"
|
||||
|
||||
|
||||
def test_section_parsing():
|
||||
view = StaticToolBoxView.from_dict({
|
||||
"id": "rna",
|
||||
"name": "RNA Seq",
|
||||
"type": "activity",
|
||||
"items": [
|
||||
{
|
||||
"type": "section",
|
||||
"name": "My Cool Section",
|
||||
"items": [
|
||||
{
|
||||
"type": "workflow",
|
||||
"id": "123456",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
section_item = view.items[0]
|
||||
assert len(section_item.items) == 1
|
||||
assert section_item.items[0].content_type == "workflow"
|
||||
|
||||
|
||||
def test_no_nested_sections():
|
||||
exception_raised = False
|
||||
try:
|
||||
StaticToolBoxView.from_dict({
|
||||
"id": "rna",
|
||||
"name": "RNA Seq",
|
||||
"type": "activity",
|
||||
"items": [
|
||||
{
|
||||
"type": "section",
|
||||
"name": "My Cool Section",
|
||||
"items": [
|
||||
{
|
||||
"type": "section",
|
||||
"name": "My Cool Inner Section",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
except pydantic.ValidationError:
|
||||
exception_raised = True
|
||||
assert exception_raised
|
||||
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from galaxy.tool_util.toolbox.views.sources import StaticToolBoxViewSources
|
||||
from galaxy.util import safe_makedirs
|
||||
from .test_toolbox_view_parsing import EXAMPLE_1
|
||||
|
||||
|
||||
def test_from_dicts():
|
||||
sources = StaticToolBoxViewSources(
|
||||
view_dicts=[EXAMPLE_1]
|
||||
)
|
||||
defs = sources.get_definitions()
|
||||
assert len(defs) == 1
|
||||
assert defs[0].id == "rna"
|
||||
|
||||
|
||||
def test_from_directory():
|
||||
target_dir = tempfile.mkdtemp()
|
||||
dir_1 = os.path.join(target_dir, "dir1")
|
||||
dir_2 = os.path.join(target_dir, "dir2")
|
||||
safe_makedirs(dir_1)
|
||||
safe_makedirs(dir_2)
|
||||
with open(os.path.join(dir_1, "foo.json"), "w") as f:
|
||||
json.dump(EXAMPLE_1, f)
|
||||
with open(os.path.join(dir_2, "seqanalysis.yml"), "w") as f:
|
||||
f.write("""
|
||||
name: Sequence Analysis
|
||||
type: generic
|
||||
items:
|
||||
- type: workflow
|
||||
id: 12345abcd
|
||||
""")
|
||||
|
||||
config_param = f"{os.path.abspath(dir_1)},{os.path.abspath(dir_2)}"
|
||||
try:
|
||||
sources = StaticToolBoxViewSources(view_directories=config_param)
|
||||
defs = sources.get_definitions()
|
||||
assert len(defs) == 2
|
||||
assert defs[0].id == "rna"
|
||||
assert defs[1].id == "seqanalysis"
|
||||
finally:
|
||||
shutil.rmtree(target_dir)
|
||||
@@ -371,6 +371,10 @@ class ToolBoxTestCase(BaseToolBoxTestCase):
|
||||
assert len(section.elems) == 1
|
||||
assert next(iter(section.elems.values())).id == "github.com/galaxyproject/example/test_tool/0.2"
|
||||
|
||||
test_tool = self.toolbox.get_tool("test_tool", tool_version="0.1")
|
||||
section_pair = self.toolbox.get_section_for_tool(test_tool)
|
||||
assert section_pair == ("tid", "TID")
|
||||
|
||||
def test_group_tools_out_of_section(self):
|
||||
self._init_tool()
|
||||
self._setup_two_versions_in_config(section=False)
|
||||
|
||||
@@ -197,6 +197,11 @@ class MockAppConfig(Bunch):
|
||||
self.delay_tool_initialization = True
|
||||
self.external_chown_script = None
|
||||
|
||||
self.default_panel_view = "default"
|
||||
self.panel_views_dir = ''
|
||||
self.panel_views = {}
|
||||
self.edam_panel_views = ''
|
||||
|
||||
self.config_file = None
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user