Merge pull request #14415 from mvdbeek/dev

Merge 22.05 into dev
This commit is contained in:
Marius van den Beek
2022-08-09 16:43:45 +02:00
committed by GitHub
41 changed files with 821 additions and 437 deletions
@@ -57,6 +57,7 @@
:is-history-item="isHistoryItem"
:is-visible="item.visible"
:state="state"
:item-urls="itemUrls"
@delete="$emit('delete')"
@display="onDisplay"
@edit="onEdit"
@@ -85,6 +86,7 @@
v-if="expandDataset"
:dataset="item"
:show-highlight="isHistoryItem"
:item-urls="itemUrls"
@edit="onEdit"
@toggleHighlights="toggleHighlights" />
</b-collapse>
@@ -169,6 +171,26 @@ export default {
tagsDisabled() {
return !this.expandDataset || !this.isHistoryItem;
},
isCollection() {
return "collection_type" in this.item;
},
/** Relative URLs for history item actions */
itemUrls() {
const id = this.item.id;
if (this.isCollection) {
return {
edit: `/collection/edit/${id}`,
};
}
return {
display: `/datasets/${id}/display/?preview=True`,
edit: `/datasets/edit/${id}`,
showDetails: `/datasets/${id}/details`,
reportError: `/datasets/error?dataset_id=${id}`,
rerun: `/tool_runner/rerun?id=${id}`,
visualize: `/visualizations?dataset_id=${id}`,
};
},
},
methods: {
onClick() {
@@ -179,8 +201,7 @@ export default {
}
},
onDisplay() {
const url = `datasets/${this.item.id}/display/?preview=True`;
iframeAdd({ path: url, title: this.name });
iframeAdd({ path: this.itemUrls.display, title: this.name });
},
onDragStart(evt) {
evt.dataTransfer.dropEffect = "move";
@@ -188,11 +209,7 @@ export default {
evt.dataTransfer.setData("text", JSON.stringify([this.item]));
},
onEdit() {
if (this.item.collection_type) {
this.$router.push(`/collection/edit/${this.item.id}`);
} else {
this.$router.push(`/datasets/edit/${this.item.id}`);
}
this.$router.push(this.itemUrls.edit);
},
onTags(newTags) {
this.$emit("tag-change", this.item, newTags);
@@ -4,25 +4,27 @@
v-if="isDataset"
:disabled="displayDisabled"
:title="displayButtonTitle"
class="px-1"
class="display-btn px-1"
size="sm"
variant="link"
@click.stop="$emit('display')">
:href="displayUrl"
@click.prevent.stop="$emit('display')">
<icon icon="eye" />
</b-button>
<b-button
v-if="isHistoryItem"
:disabled="editDisabled"
:title="editButtonTitle"
class="px-1"
class="edit-btn px-1"
size="sm"
variant="link"
@click.stop="$emit('edit')">
:href="editUrl"
@click.prevent.stop="$emit('edit')">
<icon icon="pen" />
</b-button>
<b-button
v-if="isHistoryItem && !isDeleted"
class="px-1"
class="delete-btn px-1"
title="Delete"
size="sm"
variant="link"
@@ -31,7 +33,7 @@
</b-button>
<b-button
v-if="isHistoryItem && isDeleted"
class="px-1"
class="undelete-btn px-1"
title="Undelete"
size="sm"
variant="link"
@@ -40,7 +42,7 @@
</b-button>
<b-button
v-if="isHistoryItem && !isVisible"
class="px-1"
class="unhide-btn px-1"
title="Unhide"
size="sm"
variant="link"
@@ -51,6 +53,7 @@
</template>
<script>
import { prependPath } from "utils/redirect.js";
export default {
props: {
isDataset: { type: Boolean, required: true },
@@ -58,6 +61,7 @@ export default {
isHistoryItem: { type: Boolean, required: true },
isVisible: { type: Boolean, default: true },
state: { type: String, default: "" },
itemUrls: { type: Object, required: true },
},
computed: {
displayButtonTitle() {
@@ -78,6 +82,12 @@ export default {
editDisabled() {
return ["discarded", "new", "upload", "queued", "running", "waiting"].includes(this.state);
},
displayUrl() {
return prependPath(this.itemUrls.display);
},
editUrl() {
return prependPath(this.itemUrls.edit);
},
},
};
</script>
@@ -2,7 +2,14 @@
<div class="dataset-actions mb-1">
<div class="clearfix">
<div class="btn-group float-left">
<b-button v-if="showError" class="px-1" title="Error" size="sm" variant="link" @click.stop="onError">
<b-button
v-if="showError"
class="px-1"
title="Error"
size="sm"
variant="link"
:href="reportErrorUrl"
@click.prevent.stop="onError">
<span class="fa fa-bug" />
</b-button>
<dataset-download v-if="showDownloads" :item="item" @on-download="onDownload" />
@@ -21,7 +28,8 @@
title="Dataset Details"
size="sm"
variant="link"
@click.stop="onInfo">
:href="showDetailsUrl"
@click.prevent.stop="onInfo">
<span class="fa fa-info-circle" />
</b-button>
<b-button
@@ -30,7 +38,8 @@
title="Run Job Again"
size="sm"
variant="link"
@click.stop="onRerun">
:href="rerunUrl"
@click.prevent.stop="onRerun">
<span class="fa fa-redo" />
</b-button>
<b-button
@@ -39,7 +48,8 @@
title="Visualize"
size="sm"
variant="link"
@click.stop="onVisualize">
:href="visualizeUrl"
@click.prevent.stop="onVisualize">
<span class="fa fa-bar-chart-o" />
</b-button>
<b-button
@@ -60,9 +70,8 @@
</template>
<script>
import { iframeAdd } from "components/plugins/legacyNavigation";
import { copy as sendToClipboard } from "utils/clipboard";
import { absPath } from "utils/redirect";
import { absPath, prependPath } from "utils/redirect.js";
import { downloadUrlMixin } from "./mixins.js";
import DatasetDownload from "./DatasetDownload";
@@ -74,6 +83,7 @@ export default {
props: {
item: { type: Object, required: true },
showHighlight: { type: Boolean, default: false },
itemUrls: { type: Object, required: true },
},
computed: {
showDownloads() {
@@ -97,6 +107,18 @@ export default {
// TODO: Check hasViz, if visualizations are activated in the config
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
},
reportErrorUrl() {
return prependPath(this.itemUrls.reportError);
},
showDetailsUrl() {
return prependPath(this.itemUrls.showDetails);
},
rerunUrl() {
return prependPath(this.itemUrls.rerun);
},
visualizeUrl() {
return prependPath(this.itemUrls.visualize);
},
},
methods: {
onCopyLink() {
@@ -107,21 +129,26 @@ export default {
window.location.href = resource;
},
onError() {
this.$router.push(`/datasets/${this.item.id}/error`);
this.$router.push(this.itemUrls.reportError);
},
onInfo() {
this.$router.push(`/datasets/${this.item.id}/details`);
this.$router.push(this.itemUrls.showDetails);
},
onRerun() {
this.$router.push(`/root?job_id=${this.item.creating_job}`);
},
onVisualize() {
const name = this.item.name || "...";
iframeAdd({
title: `Visualization of ${name}`,
path: `/visualizations?dataset_id=${this.item.id}`,
$router: this.$router,
});
const name = this.item.name || "";
const title = `Visualization of ${name}`;
const path = this.itemUrls.visualize;
const redirectParams = {
path: path,
title: title,
tryIframe: false,
};
if (!this.iframeAdd(redirectParams)) {
this.$router.push(path);
}
},
onHighlight() {
this.$emit("toggleHighlights");
@@ -21,7 +21,11 @@
<span class="value">{{ result.misc_info }}</span>
</div>
</div>
<DatasetActions :item="result" :show-highlight="showHighlight" @toggleHighlights="toggleHighlights" />
<DatasetActions
:item="result"
:show-highlight="showHighlight"
:item-urls="itemUrls"
@toggleHighlights="toggleHighlights" />
<pre v-if="result.peek" class="dataset-peek p-1" v-html="result.peek" />
</div>
</div>
@@ -94,6 +98,7 @@ export default {
props: {
dataset: { type: Object, required: true },
showHighlight: { type: Boolean, default: false },
itemUrls: { type: Object, required: true },
},
computed: {
stateText() {
@@ -14,12 +14,15 @@
<template v-slot:button-content>
<span class="fa fa-save" />
</template>
<b-dropdown-item v-localize @click.stop="onDownload(downloadUrl)"> Download Dataset </b-dropdown-item>
<b-dropdown-item v-localize :href="downloadUrl" @click.prevent.stop="onDownload(downloadUrl)">
Download Dataset
</b-dropdown-item>
<b-dropdown-item
v-for="(metaFile, index) of metaFiles"
:key="index"
:data-description="`download ${metaFile.file_type}`"
@click.stop="onDownload(metaDownloadUrl, metaFile.file_type)">
:href="metaDownloadUrl + metaFile.file_type"
@click.prevent.stop="onDownload(metaDownloadUrl, metaFile.file_type)">
Download {{ metaFile.file_type }}
</b-dropdown-item>
</b-dropdown>
@@ -29,7 +32,8 @@
title="Download"
size="sm"
variant="link"
@click.stop="onDownload(downloadUrl)">
:href="downloadUrl"
@click.prevent.stop="onDownload(downloadUrl)">
<span class="fa fa-save" />
</b-button>
</template>
@@ -13,7 +13,7 @@
<b-dropdown-text id="history-op-all-content">
<span v-localize>With entire history...</span>
</b-dropdown-text>
<b-dropdown-item v-if="numItemsActive" data-description="copy datasets" @click="onCopy">
<b-dropdown-item data-description="copy datasets" @click="onCopy">
<span v-localize>Copy Datasets</span>
</b-dropdown-item>
<b-dropdown-item v-if="numItemsHidden" v-b-modal:show-all-hidden-content>
@@ -9,11 +9,13 @@ export const getUserPreferencesModel = (user_id) => {
information: {
title: _l("Manage Information"),
id: "edit-preferences-information",
description: _l("Edit your email, addresses and custom parameters or change your public name."),
description: config.enable_account_interface
? _l("Edit your email, addresses and custom parameters or change your public name.")
: _l("Edit your custom parameters."),
url: `api/users/${user_id}/information/inputs`,
icon: "fa-user",
redirect: "user",
shouldRender: !config.use_remote_user && config.enable_account_interface,
shouldRender: !config.use_remote_user,
},
password: {
title: _l("Change Password"),
@@ -69,7 +71,7 @@ export const getUserPreferencesModel = (user_id) => {
icon: "fa-cloud",
submitTitle: "Create a new Key",
submitIcon: "fa-check",
shouldRender: config.enable_account_interface,
shouldRender: true,
},
toolbox_filters: {
title: _l("Manage Toolbox Filters"),
@@ -88,7 +88,7 @@ export default {
deleteHistory: (history) => this.deleteHistory({ history }),
deleteCurrentHistory: () => this.deleteHistory({ history: this.currentHistory }),
// purge history then clearn currentHistoryId
// purge history then clear currentHistoryId
purgeHistory: (history) => this.deleteHistory({ history, purge: true }),
purgeCurrentHistory: () => this.deleteHistory({ history: this.currentHistory, purge: true }),
@@ -32,7 +32,14 @@ const mutations = {
Vue.delete(state.histories, doomed.id);
},
setHistories(state, newHistories = []) {
const currentHistoryId = state.currentHistoryId;
const currentHistory = state.histories[currentHistoryId];
const newMap = newHistories.reduce((acc, h) => ({ ...acc, [h.id]: h }), {});
if (currentHistory) {
// The incoming history list contains less information than the current history
// so we restore the existing current history since it gets updated regularly anyway
newMap[currentHistoryId] = currentHistory;
}
Vue.set(state, "histories", newMap);
},
setHistoriesLoading(state, isLoading) {
@@ -145,10 +152,8 @@ const actions = {
commit("setCurrentHistoryId", history.id);
},
async setCurrentHistory({ dispatch, getters }, id) {
if (id !== getters.currentHistoryId) {
const changedHistory = await setCurrentHistoryOnServer(id);
return dispatch("selectHistory", changedHistory);
}
const changedHistory = await setCurrentHistoryOnServer(id);
return dispatch("selectHistory", changedHistory);
},
setHistory({ commit }, history) {
commit("setHistory", history);
@@ -7,6 +7,7 @@
import defaultStore from "store/index";
import { urlData } from "utils/url";
import { loadSet } from "utils/setCache";
import { getCurrentHistoryFromServer } from "./queries";
import { getGalaxyInstance } from "app";
@@ -52,18 +53,19 @@ export async function watchHistoryOnce(store) {
lastUpdateTime = history.update_time;
// execute request to obtain recently changed items
const params = {
v: "dev",
limit: limit,
q: "update_time-ge",
qv: lastRequestDate.toISOString(),
v: "dev",
view: "detailed",
};
const paramsString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join("&");
const url = `api/histories/${historyId}/contents?${paramsString}`;
// request detailed info only for the expanded datasets
const detailedIds = getCurrentlyExpandedHistoryContentIds();
if (detailedIds.length) {
params["details"] = detailedIds.join(",");
}
const url = `api/histories/${historyId}/contents`;
lastRequestDate = new Date();
const payload = await urlData({ url });
const payload = await urlData({ url, params });
// show warning that not all changes have been obtained
if (payload && payload.length == limit) {
console.debug(`Reached limit of monitored changes (limit=${limit}).`);
@@ -110,3 +112,22 @@ export function rewatchHistory() {
watchHistory();
}
}
/**
* Returns the set of history item IDs that are currently expanded in the history panel from the cache.
* These content items need to retrieve detailed information when updated.
* @returns {Array<string>} List of history item IDs that are currently expanded.
*/
function getCurrentlyExpandedHistoryContentIds() {
const expandedItemIds = [];
const cacheKey = "expanded-history-items";
const expandedItems = loadSet(cacheKey);
expandedItems.forEach((key) => {
// Items have the format: <type>-<id>
const itemId = key.split("-")[1];
if (itemId) {
expandedItemIds.push(itemId);
}
});
return expandedItemIds;
}
+2 -3
View File
@@ -290,9 +290,8 @@ history_panel:
options_use_legacy_history: 'a[data-description="switch to legacy history view"]'
collection_menu_button: '.collection-menu'
collection_menu_edit_attributes:
type: xpath
selector: '//button[@title="Edit attributes"]'
collection_menu_edit_attributes: '.edit-btn'
new_history_button: '.history-new-button'
multi_view_button: '.history-view-multi-button'
histories_operation_menu: '[data-description="history options"]'
+20 -2
View File
@@ -10,15 +10,33 @@ export function reloadPage() {
window.location.reload();
}
// Prepends configured appRoot to given url
const slashCleanup = /(\/)+/g;
/**
* Prepends the configured app root to given url
* @param {String} path
* @returns The relative URL path with the configured appRoot.
*/
export function prependPath(path) {
const root = getAppRoot();
return `${root}/${path}`.replace(slashCleanup, "/");
}
/**
* Returns the absolute URL path for this server given a relative path.
* @param {String} path
* @returns The absolute URL path.
*/
export function absPath(path) {
const relativePath = prependPath(path);
const relativePath = hasRoot(path) ? path : prependPath(path);
const server = window.location.origin;
return `${server}/${relativePath}`.replace(slashCleanup, "/");
}
/**
* Checks if the path already has the app root.
* @param {String} path
* @returns true if the given path starts with the app root.
*/
function hasRoot(path) {
return path.startsWith(getAppRoot());
}
+3 -2
View File
@@ -2,11 +2,12 @@ import axios from "axios";
import { getAppRoot } from "onload/loadConfig";
import { rethrowSimple } from "utils/simple-error";
export async function urlData({ url, headers }) {
export async function urlData({ url, headers, params }) {
try {
console.debug("Requesting data from: ", url);
headers = headers || {};
const { data } = await axios.get(`${getAppRoot()}${url}`, { headers });
params = params || {};
const { data } = await axios.get(`${getAppRoot()}${url}`, { headers, params });
return data;
} catch (e) {
rethrowSimple(e);
+6 -6
View File
@@ -92,29 +92,29 @@ steps:
grey this means you are queued and need to wait until your tool can be started. If your dataset turns into red, an error has occurred. Please report the bug to the Galaxy team with the bug report button."
- title: "View dataset"
element: "#current-history-panel div.content-item button[title='Display']"
element: "#current-history-panel div.content-item .display-btn"
intro: "View your dataset by clicking the eye button."
- title: "Rename dataset"
element: "#current-history-panel div.content-item button[title='Edit attributes']"
element: "#current-history-panel div.content-item .edit-btn"
intro: "Rename your dataset by clicking the pencil button."
- title: "Dataset information"
element: "div.content-item .content-title"
intro: "This is your dataset. You can get more informations and options like different visualizations by clicking on it."
postclick: true
- title: "Remove dataset"
element: "#current-history-panel div.content-item button[title='Delete']"
element: "#current-history-panel div.content-item .delete-btn"
intro: "Delete your dataset by clicking the trash-button."
- title: "Dataset information"
element: "#current-history-panel div.content-item button[title='Dataset Details']"
element: "#current-history-panel div.content-item .params-btn"
intro: "Clicking on your dataset provides you with more information regarding your dataset (e.g. filetype or size)."
preclick: true
- title: "Re-run tool"
element: "#current-history-panel div.content-item button[title='Run Job Again']"
element: "#current-history-panel div.content-item .rerun-btn"
intro: "By clicking the reload button, you can re-run your tool again (e.g. with different parameters or on another dataset)."
- title: "Panel collapse"
+6 -7
View File
@@ -47,26 +47,26 @@ steps:
element: "#current-history-panel div.content-item span.datatype"
intro: "Galaxy has assigned a datatype to your dataset during upload, which you can see here."
- element: "#current-history-panel div.content-item button[title='Download']"
- element: "#current-history-panel div.content-item .download-btn"
title: "Download your dataset"
intro: "You can download every dataset by using the floppy disc symbol."
- element: "#current-history-panel div.content-item button[title='Dataset Details']"
- element: "#current-history-panel div.content-item .params-btn"
title: "Even more information"
intro: "Get an overview of all metadata associated with your dataset by using the Information symbol."
preclick: true
- element: "#current-history-panel div.content-item button[title='Display']"
- element: "#current-history-panel div.content-item .display-btn"
title: "Inspect your data"
intro: "The eye symbol can be used to look at your data."
preclick: true
- element: "#current-history-panel div.content-item button[title='Edit attributes']"
- element: "#current-history-panel div.content-item .edit-btn"
title: "Edit metadata"
intro: "With the pencil button you can edit metadata attributes of your dataset, like the associated filetype or the dataset name."
preclick: true
- element: "#current-history-panel div.content-item button[title='Delete']"
- element: "#current-history-panel div.content-item .delete-btn"
title: "Remove datasets"
intro: "You can remove a dataset from the history with the trash can symbol."
postclick: true
@@ -76,7 +76,7 @@ steps:
intro: "By default your history will hide all deleted datasets from you. You can visualize them by filtering."
textinsert: "deleted:true"
- element: "#current-history-panel div.content-item button[title='Undelete']"
- element: "#current-history-panel div.content-item .undelete-btn"
title: "Undeleting a dataset"
intro: |
Galaxy datasets are only marked as deleted and can be recovered by clicking this link.
@@ -95,4 +95,3 @@ steps:
- title: "Enjoy your Galaxy Histories"
intro: "Thanks for taking this tour! Happy research with Galaxy!"
+1 -1
View File
@@ -54,7 +54,7 @@ steps:
- element: "#right"
intro: "This is your history. It contains all datasets you are currently working with including our uploaded table."
- element: "#current-history-panel div.content-item button[title='Display']"
- element: "#current-history-panel div.content-item .display-btn"
intro: "Clicking the eye-icon usually displays a dataset in the center panel."
postclick: true
+1 -1
View File
@@ -138,7 +138,7 @@
if (tool_id === "upload1" || tool_id === "upload") {
document.getElementById("tool-panel-upload-button").click();
} else {
Galaxy.router.push("/", { tool_id: tool_id });
Galaxy.router.push("/", { tool_id: encodeURI(tool_id) });
}
removeOverlay();
});
+66 -39
View File
@@ -3049,11 +3049,20 @@
~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
:Default: ``9.0``
In tool search, a query match against a tool's name text will
receive this score multiplier.
:Default: ``20.0``
:Type: float
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_name_exact_multiplier``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
If a search query matches a tool name exactly, the score will be
multiplied by this factor.
:Default: ``10.0``
:Type: float
@@ -3062,11 +3071,10 @@
~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
:Default: ``9.0``
In tool search, a query match against a tool's ID text will
receive this score multiplier. The query must be an exact match
against ID in order to be counted as a match.
:Default: ``20.0``
:Type: float
@@ -3075,10 +3083,8 @@
~~~~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
In tool search, a query match against a tool's section text will
receive this score multiplier.
:Default: ``3.0``
:Type: float
@@ -3088,11 +3094,9 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
:Default: ``2.0``
In tool search, a query match against a tool's description text
will receive this score multiplier.
:Default: ``8.0``
:Type: float
@@ -3101,10 +3105,8 @@
~~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
In tool search, a query match against a tool's label text will
receive this score multiplier.
:Default: ``1.0``
:Type: float
@@ -3114,11 +3116,10 @@
~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
:Default: ``5.0``
A stub is parsed from the GUID as "owner/repo/tool_id". In tool
search, a query match against a tool's stub text will receive this
score multiplier.
:Default: ``2.0``
:Type: float
@@ -3127,10 +3128,22 @@
~~~~~~~~~~~~~~~~~~~
:Description:
Boosts are used to customize this instance's toolbox search. The
higher the boost, the more importance the scoring algorithm gives
to the given field. Section refers to the tool group in the tool
panel. Rest of the fields are tool's attributes.
In tool search, a query match against a tool's help text will
receive this score multiplier.
:Default: ``1.0``
:Type: float
~~~~~~~~~~~~~~~~~~~~~~
``tool_help_bm25f_k1``
~~~~~~~~~~~~~~~~~~~~~~
:Description:
The lower this parameter, the greater the diminishing reward for
term frequency in the help text. A higher K1 increases the level
of reward for additional occurences of a term. The default value
will provide a slight increase in score for the first, second and
third occurrence and little reward thereafter.
:Default: ``0.5``
:Type: float
@@ -3140,8 +3153,8 @@
~~~~~~~~~~~~~~~~~~~~~
:Description:
Limits the number of results in toolbox search. Can be used to
tweak how many results will appear.
Limits the number of results in toolbox search. Use to set the
maximum number of tool search results to display.
:Default: ``20``
:Type: int
@@ -3151,9 +3164,11 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
Enable/ disable Ngram-search for tools. It makes tool search
results tolerant for spelling mistakes in the query by dividing
the query into multiple ngrams and search for each ngram
Disabling this will prevent partial matches on tool names.
Enable/disable Ngram-search for tools. It makes tool search
results tolerant for spelling mistakes in the query, and will also
match query substrings e.g. "genome" will match "genomics" or
"metagenome".
:Default: ``true``
:Type: bool
@@ -3163,7 +3178,7 @@
~~~~~~~~~~~~~~~~~~~~~~
:Description:
Set minimum size of ngrams
Set minimum character length of ngrams
:Default: ``3``
:Type: int
@@ -3173,11 +3188,23 @@
~~~~~~~~~~~~~~~~~~~~~~
:Description:
Set maximum size of ngrams
Set maximum character length of ngrams
:Default: ``4``
:Type: int
~~~~~~~~~~~~~~~~~~~~~
``tool_ngram_factor``
~~~~~~~~~~~~~~~~~~~~~
:Description:
Ngram matched scores will be multiplied by this factor. Should
always be below 1, because an ngram match is a partial match of a
search term.
:Default: ``0.2``
:Type: float
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_test_data_directories``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+46 -40
View File
@@ -1565,63 +1565,69 @@ galaxy:
# Available formats are currently 'zip', 'gz', and 'bz2'.
#disable_library_comptypes: null
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
#tool_name_boost: 9.0
# In tool search, a query match against a tool's name text will
# receive this score multiplier.
#tool_name_boost: 20.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
#tool_id_boost: 9.0
# If a search query matches a tool name exactly, the score will be
# multiplied by this factor.
#tool_name_exact_multiplier: 10.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
# In tool search, a query match against a tool's ID text will receive
# this score multiplier. The query must be an exact match against ID
# in order to be counted as a match.
#tool_id_boost: 20.0
# In tool search, a query match against a tool's section text will
# receive this score multiplier.
#tool_section_boost: 3.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
#tool_description_boost: 2.0
# In tool search, a query match against a tool's description text will
# receive this score multiplier.
#tool_description_boost: 8.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
# In tool search, a query match against a tool's label text will
# receive this score multiplier.
#tool_label_boost: 1.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
#tool_stub_boost: 5.0
# A stub is parsed from the GUID as "owner/repo/tool_id". In tool
# search, a query match against a tool's stub text will receive this
# score multiplier.
#tool_stub_boost: 2.0
# Boosts are used to customize this instance's toolbox search. The
# higher the boost, the more importance the scoring algorithm gives to
# the given field. Section refers to the tool group in the tool
# panel. Rest of the fields are tool's attributes.
#tool_help_boost: 0.5
# In tool search, a query match against a tool's help text will
# receive this score multiplier.
#tool_help_boost: 1.0
# Limits the number of results in toolbox search. Can be used to
# tweak how many results will appear.
# The lower this parameter, the greater the diminishing reward for
# term frequency in the help text. A higher K1 increases the level of
# reward for additional occurences of a term. The default value will
# provide a slight increase in score for the first, second and third
# occurrence and little reward thereafter.
#tool_help_bm25f_k1: 0.5
# Limits the number of results in toolbox search. Use to set the
# maximum number of tool search results to display.
#tool_search_limit: 20
# Enable/ disable Ngram-search for tools. It makes tool search results
# tolerant for spelling mistakes in the query by dividing the query
# into multiple ngrams and search for each ngram
# Disabling this will prevent partial matches on tool names.
# Enable/disable Ngram-search for tools. It makes tool search results
# tolerant for spelling mistakes in the query, and will also match
# query substrings e.g. "genome" will match "genomics" or
# "metagenome".
#tool_enable_ngram_search: true
# Set minimum size of ngrams
# Set minimum character length of ngrams
#tool_ngram_minsize: 3
# Set maximum size of ngrams
# Set maximum character length of ngrams
#tool_ngram_maxsize: 4
# Ngram matched scores will be multiplied by this factor. Should
# always be below 1, because an ngram match is a partial match of a
# search term.
#tool_ngram_factor: 0.2
# Set tool test data directory. The test framework sets this value to
# 'test-data,https://github.com/galaxyproject/galaxy-test-data.git'
# which will cause Galaxy to clone down extra test data on the fly for
+59 -40
View File
@@ -2206,25 +2206,31 @@ mapping:
tool_name_boost:
type: float
default: 9.0
default: 20.0
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
In tool search, a query match against a tool's name text will receive
this score multiplier.
tool_name_exact_multiplier:
type: float
default: 10.0
required: false
reloadable: true
desc: |
If a search query matches a tool name exactly, the score will be
multiplied by this factor.
tool_id_boost:
type: float
default: 9.0
default: 20.0
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
In tool search, a query match against a tool's ID text will receive
this score multiplier. The query must be an exact match against ID
in order to be counted as a match.
tool_section_boost:
type: float
@@ -2232,21 +2238,17 @@ mapping:
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
In tool search, a query match against a tool's section text will
receive this score multiplier.
tool_description_boost:
type: float
default: 2.0
default: 8.0
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
In tool search, a query match against a tool's description text will
receive this score multiplier.
tool_label_boost:
type: float
@@ -2254,32 +2256,39 @@ mapping:
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
In tool search, a query match against a tool's label text will
receive this score multiplier.
tool_stub_boost:
type: float
default: 5.0
default: 2.0
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
A stub is parsed from the GUID as "owner/repo/tool_id".
In tool search, a query match against a tool's stub text will receive
this score multiplier.
tool_help_boost:
type: float
default: 1.0
required: false
reloadable: true
desc: |
In tool search, a query match against a tool's help text will receive
this score multiplier.
tool_help_bm25f_k1:
type: float
default: 0.5
required: false
reloadable: true
desc: |
Boosts are used to customize this instance's toolbox search.
The higher the boost, the more importance the scoring algorithm gives to the
given field. Section refers to the tool group in the tool panel. Rest of
the fields are tool's attributes.
The lower this parameter, the greater the diminishing reward for
term frequency in the help text. A higher K1 increases the level
of reward for additional occurences of a term. The default value will
provide a slight increase in score for the first, second and third
occurrence and little reward thereafter.
tool_search_limit:
type: int
@@ -2287,8 +2296,8 @@ mapping:
required: false
reloadable: true
desc: |
Limits the number of results in toolbox search. Can be used to tweak how many
results will appear.
Limits the number of results in toolbox search. Use to set the
maximum number of tool search results to display.
tool_enable_ngram_search:
type: bool
@@ -2296,10 +2305,11 @@ mapping:
required: false
reloadable: true
desc: |
Enable/ disable Ngram-search for tools. It makes tool
search results tolerant for spelling mistakes in the query
by dividing the query into multiple ngrams and search for
each ngram
Disabling this will prevent partial matches on tool names.
Enable/disable Ngram-search for tools. It makes tool
search results tolerant for spelling mistakes in the query, and will
also match query substrings e.g. "genome" will match "genomics" or
"metagenome".
tool_ngram_minsize:
type: int
@@ -2307,7 +2317,7 @@ mapping:
required: false
reloadable: true
desc: |
Set minimum size of ngrams
Set minimum character length of ngrams
tool_ngram_maxsize:
type: int
@@ -2315,7 +2325,16 @@ mapping:
required: false
reloadable: true
desc: |
Set maximum size of ngrams
Set maximum character length of ngrams
tool_ngram_factor:
type: float
default: 0.2
required: false
reloadable: true
desc: |
Ngram matched scores will be multiplied by this factor. Should always
be below 1, because an ngram match is a partial match of a search term.
tool_test_data_directories:
type: str
+3 -3
View File
@@ -191,7 +191,7 @@ class DatasetSerializer(base.ModelSerializer[DatasetManager], deletable.Purgable
"extra_files_path": self.serialize_extra_files_path,
"permissions": self.serialize_permissions,
"total_size": lambda item, key, **context: int(item.get_total_size()),
"file_size": lambda item, key, **context: int(item.get_size()),
"file_size": lambda item, key, **context: int(item.get_size(calculate_size=False)),
}
self.serializers.update(serializers)
@@ -526,9 +526,9 @@ class _UnflattenedMetadataDatasetAssociationSerializer(base.ModelSerializer[T],
"extra_files_path": self._proxy_to_dataset(serializer=self.dataset_serializer.serialize_extra_files_path),
"permissions": self._proxy_to_dataset(serializer=self.dataset_serializer.serialize_permissions),
# TODO: do the sizes proxy accurately/in the same way?
"size": lambda item, key, **context: int(item.get_size()),
"size": lambda item, key, **context: int(item.get_size(calculate_size=False)),
"file_size": lambda item, key, **context: self.serializers["size"](item, key, **context),
"nice_size": lambda item, key, **context: item.get_size(nice_size=True),
"nice_size": lambda item, key, **context: item.get_size(nice_size=True, calculate_size=False),
# common to lddas and hdas - from mapping.py
"copied_from_history_dataset_association_id": self.serialize_id,
"copied_from_library_dataset_dataset_association_id": self.serialize_id,
+1 -1
View File
@@ -242,7 +242,7 @@ class LibraryDatasetsManager(datasets.DatasetAssociationManager):
rval["deleted"] = ld.deleted
rval["folder_id"] = f"F{rval['folder_id']}"
rval["full_path"] = full_path
rval["file_size"] = util.nice_size(int(ldda.get_size()))
rval["file_size"] = util.nice_size(int(ldda.get_size(calculate_size=False)))
rval["date_uploaded"] = ldda.create_time.isoformat()
rval["update_time"] = ldda.update_time.isoformat()
rval["can_user_modify"] = trans.user_is_admin or trans.app.security_agent.can_modify_library_item(
+8 -5
View File
@@ -3569,18 +3569,21 @@ class Dataset(Base, StorableObject, Serializable, _HasTable):
else:
return self.object_store.size(self)
def get_size(self, nice_size=False):
def get_size(self, nice_size=False, calculate_size=True):
"""Returns the size of the data on disk"""
if self.file_size:
if nice_size:
return galaxy.util.nice_size(self.file_size)
else:
return self.file_size
else:
elif calculate_size:
# Hopefully we only reach this branch in sessionless mode
if nice_size:
return galaxy.util.nice_size(self._calculate_size())
else:
return self._calculate_size()
else:
return self.file_size or 0
def set_size(self, no_extra_files=False):
"""Sets the size of the data on disk.
@@ -4021,11 +4024,11 @@ class DatasetInstance(UsesCreateAndUpdateTime, _HasTable):
self.clear_associated_files()
_get_datatypes_registry().change_datatype(self, new_ext)
def get_size(self, nice_size=False):
def get_size(self, nice_size=False, calculate_size=True):
"""Returns the size of the data on disk"""
if nice_size:
return galaxy.util.nice_size(self.dataset.get_size())
return self.dataset.get_size()
return galaxy.util.nice_size(self.dataset.get_size(calculate_size=calculate_size))
return self.dataset.get_size(calculate_size=calculate_size)
def set_size(self, **kwds):
"""Sets and gets the size of the data on disk"""
+66 -3
View File
@@ -14,7 +14,10 @@ from alembic.script import ScriptDirectory
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from galaxy.model.database_utils import is_one_database
from galaxy.model.database_utils import (
database_exists,
is_one_database,
)
from galaxy.model.migrations import (
AlembicManager,
DatabaseConfig,
@@ -38,6 +41,51 @@ GXY_CONFIG_PREFIX = "GALAXY_CONFIG_"
TSI_CONFIG_PREFIX = "GALAXY_INSTALL_CONFIG_"
class DatabaseDoesNotExistError(Exception):
def __init__(self, db_url: str) -> None:
super().__init__(
f"""The database at {db_url} does not exist. You must
create and initialize the database before running this script. You
can do so by (a) running `create_db.sh`; or by (b) starting Galaxy,
in which case Galaxy will create and initialize the database
automatically."""
)
class DatabaseNotInitializedError(Exception):
def __init__(self, db_url: str) -> None:
super().__init__(
f"""The database at {db_url} is empty. You must
initialize the database before running this script. You can do so by
(a) running `create_db.sh`; or by (b) starting Galaxy, in which case
Galaxy will initialize the database automatically."""
)
def verify_database_is_initialized(db_url: str) -> None:
"""
Intended for use by scripts that run database migrations (manage_db.sh,
run_alembic.sh). Those scripts are meant to run on a database that has been
initialized with the appropriate metadata (e.g. galaxy or install model).
This function will raise an error if the database does not exist or has not
been initialized*.
*NOTE: this function cannot determine whether a database has been properly
initialized; it can only tell when a database has *not* been initialized.
"""
if not database_exists(db_url):
raise DatabaseDoesNotExistError(db_url)
engine = create_engine(db_url)
try:
db_state = DatabaseStateCache(engine=engine)
if db_state.is_database_empty() or db_state.contains_only_kombu_tables():
raise DatabaseNotInitializedError(db_url)
finally:
engine.dispose()
def get_configuration(argv: List[str], cwd: str) -> Tuple[DatabaseConfig, DatabaseConfig, bool]:
"""
Return a 3-item-tuple with configuration values used for managing databases.
@@ -120,7 +168,15 @@ class LegacyScripts:
def __init__(self, argv: List[str], cwd: Optional[str] = None) -> None:
self.argv = argv
self.cwd = cwd or os.getcwd()
self.database = self.DEFAULT_DB_ARG
self._database: Optional[str] = None # Do not assign default value: `None` means we don't know yet.
@property
def database(self):
if self._database is None:
raise LegacyScriptsException(
"Attempt to access identifier of database before processing the script arguments"
)
return self._database
def run(self) -> None:
"""
@@ -147,8 +203,9 @@ class LegacyScripts:
If last argument is a valid database name, pop and assign it; otherwise assign default.
"""
arg = self.argv[-1]
self._database = self.DEFAULT_DB_ARG
if arg in ["galaxy", "install"]:
self.database = self.argv.pop()
self._database = self.argv.pop()
def rename_config_argument(self) -> None:
"""
@@ -196,6 +253,12 @@ class LegacyScripts:
elif self.database == "install":
self.argv.append("tsi@head")
def get_db_url(self):
if self.database in ["galaxy", self.DEFAULT_DB_ARG]:
return self.gxy_url
elif self.database == "install":
return self.tsi_url
def _rename_arg(self, old_name, new_name) -> None:
pos = self.argv.index(old_name)
self.argv[pos] = new_name
+12 -3
View File
@@ -3987,7 +3987,7 @@ exclusively use ``filter``s to populate options.
* ``from_dataset`` - The options for the select list are dynamically obtained
from input dataset selected for the tool from the current history.
* ``from_file`` - The options for the select list are dynamically obtained from
a file. This mechanis is discourage in favor of the more generic
a file. This mechanism is discouraged in favor of the more generic
``from_data_table``.
* ``from_parameter`` - The options for the select list are dynamically obtained
from a parameter.
@@ -4063,6 +4063,10 @@ Starting from Galaxy v21.01, ``meta_file_key`` can be used together with
medadata file that the ``meta_file_key`` implies, instead of the dataset
itself.
Note that in any case only the first mega byte of the referred dataset (or file)
is considered. Lines starting with ``#`` are ignored. By using the ``startswith``
attribute also lines starting with other strings can be ignored.
```xml
<param name="input" type="data" format="maf" label="MAF File"/>
<param name="species" type="select" optional="False" label="Select species for the input dataset" multiple="True">
@@ -4137,7 +4141,7 @@ used to generate dynamic options.
</xs:sequence>
<xs:attribute name="from_dataset" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Determine options from the dataset given in the referred input parameter. If 'meta_file_key' is given, the options are determined from the data in the metadata file of the input.</xs:documentation>
<xs:documentation xml:lang="en">Determine options from (the first MB of) the dataset given in the referred input parameter. If 'meta_file_key' is given, the options are determined from (the first MB of) the data in the metadata file of the input.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="from_file" type="xs:string">
@@ -4167,7 +4171,7 @@ used to generate dynamic options.
</xs:attribute>
<xs:attribute name="startswith" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Ignore lines starting with the given character.</xs:documentation>
<xs:documentation xml:lang="en">Ignore lines starting with the given string.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="meta_file_key" type="xs:string">
@@ -4175,6 +4179,11 @@ used to generate dynamic options.
<xs:documentation xml:lang="en">Works with from_dataset only. See [docs](#from-dataset)</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="separator" type="xs:string">
<xs:annotation>
<xs:documentation xml:lang="en">Split tabular data with this character (default is tab)</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:group name="OptionsElement">
<xs:choice>
+5 -5
View File
@@ -249,20 +249,20 @@ IMPLICITLY_REQUIRED_TOOL_FILES: Dict[str, Dict] = {
# "foobar": {"required": REQUIRE_FULL_DIRECTORY}
# if no version is specified, all versions without explicit RequiredFiles will be selected
"circos": {"required": REQUIRE_FULL_DIRECTORY},
"cp_image_math": {"required": {"includes": [{"path": "cp_common_functions.py", "path_type": "literal"}]}},
"enumerate_charges": {"required": {"includes": [{"path": "site_substructures.smarts", "path_type": "literal"}]}},
"cp_image_math": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"enumerate_charges": {"required": REQUIRE_FULL_DIRECTORY},
"fasta_compute_length": {"required": {"includes": [{"path": "utils/*", "path_type": "glob"}]}},
"fasta_concatenate0": {"required": {"includes": [{"path": "utils/*", "path_type": "glob"}]}},
"filter_tabular": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"flanking_features_1": {"required": {"includes": [{"path": "utils/*", "path_type": "glob"}]}},
"gops_intersect_1": {"required": {"includes": [{"path": "utils/*", "path_type": "glob"}]}},
"gops_subtract_1": {"required": {"includes": [{"path": "utils/*", "path_type": "glob"}]}},
"maxquant": {"required": {"includes": [{"path": "mqparam.py", "path_type": "literal"}]}},
"maxquant_mqpar": {"required": {"includes": [{"path": "mqparam.py", "path_type": "literal"}]}},
"maxquant": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"maxquant_mqpar": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"query_tabular": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"shasta": {"required": {"includes": [{"path": "configs/*", "path_type": "glob"}]}},
"sqlite_to_tabular": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
"sucos_max_score": {"required": {"includes": [{"path": "utils.py", "path_type": "literal"}]}},
"sucos_max_score": {"required": {"includes": [{"path": "*.py", "path_type": "glob"}]}},
}
@@ -655,7 +655,7 @@ class DynamicOptions:
rval = []
field_count = None
for line in reader:
if line.startswith("#") or (self.line_startswith and not line.startswith(self.line_startswith)):
if line.startswith("#") or (self.line_startswith and line.startswith(self.line_startswith)):
continue
line = line.rstrip("\n\r")
if line:
+203 -142
View File
@@ -1,16 +1,36 @@
"""
Module for building and searching the index of tools
installed within this Galaxy. Before changing index-building
or searching related parts it is deeply recommended to read
through the library docs at https://whoosh.readthedocs.io.
Module for building and searching the index of installed tools.
Before changing index-building or searching related parts it is highly
recommended to read the docs at https://whoosh.readthedocs.io.
Schema - this is how we define the index, both for building and searching. A
field is created for each data element that we want to add e.g. tool name,
tool ID, description. The type of field and its attributes define how
entries for that field will be indexed and ultimately how they can be
searched. Score weighting (boost) is added here on a per-field bases, to
allow matches to important fields like "name" to receive a higher score.
Tokenizers - these take an attribute (e.g. name) and parse it into "tokens" to
be stored in the index. Can be done in many ways for different search
functionality. For example, the IDTokenizer creates one token for an entire
entry, resulting in an index field that requires a full-field match. The
default tokenizer will break an entry into words, so that single word
matches are possible.
Filters - various filters are available for processing content as the index is
built. A StopFilter removes common articles 'a', 'for', 'and' etc. A
StemmingFilter removes suffixes from words to create a 'base work' e.g.
stemming -> stem; opened -> open; philosophy -> philosoph.
"""
import logging
import os
import re
import shutil
from typing import (
Dict,
List,
Tuple,
Union,
)
@@ -18,10 +38,10 @@ from whoosh import (
analysis,
index,
)
from whoosh.analysis import StandardAnalyzer
from whoosh.fields import (
ID,
KEYWORD,
NGRAMWORDS,
Schema,
TEXT,
)
@@ -31,10 +51,12 @@ from whoosh.qparser import (
)
from whoosh.scoring import (
BM25F,
Frequency,
MultiWeighting,
)
from whoosh.writing import AsyncWriter
from galaxy.config import GalaxyAppConfiguration
from galaxy.util import ExecutionTimer
from galaxy.web.framework.helpers import to_unicode
@@ -44,16 +66,19 @@ CanConvertToFloat = Union[str, int, float]
CanConvertToInt = Union[str, int, float]
def get_or_create_index(index_dir: str, schema: Schema) -> index.Index:
def get_or_create_index(index_dir, schema):
"""Get or create a reference to the index."""
if not os.path.exists(index_dir):
os.makedirs(index_dir)
if index.exists_in(index_dir):
idx = index.open_dir(index_dir)
try:
assert idx.schema == schema
if idx.schema == schema:
return idx
except AssertionError:
log.warning("Index at '%s' uses outdated schema, creating new index", index_dir)
log.warning(f"Index at '{index_dir}' uses outdated schema, creating a new index")
# Delete the old index and return a new index reference
shutil.rmtree(index_dir)
os.makedirs(index_dir)
return index.create_in(index_dir, schema=schema)
@@ -68,7 +93,12 @@ class ToolBoxSearch:
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(panel_view_id, panel_index_dir, index_help=index_help)
panel_searches[panel_view_id] = ToolPanelViewSearch(
panel_view_id,
panel_index_dir,
index_help=index_help,
config=toolbox.app.config,
)
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,
@@ -95,39 +125,111 @@ class ToolPanelViewSearch:
the Whoosh search library.
"""
def __init__(self, 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,
name=TEXT(analyzer=analysis.SimpleAnalyzer()),
description=TEXT,
section=TEXT,
help=TEXT,
labels=KEYWORD,
)
def __init__(
self,
panel_view_id: str,
index_dir: str,
config: GalaxyAppConfiguration,
index_help: bool = True,
):
"""Build the schema and validate against the index."""
schema_conf = {
# The stored ID field is not searchable
"id": ID(stored=True, unique=True),
# This exact field is searchable by exact matches only
"id_exact": TEXT(
field_boost=(config.tool_id_boost * config.tool_name_exact_multiplier),
analyzer=analysis.IDTokenizer() | analysis.LowercaseFilter(),
),
# The primary name field is searchable by exact match only, and is
# eligible for massive score boosting. A secondary ngram or text
# field for name is added below
"name_exact": TEXT(
field_boost=(config.tool_name_boost * config.tool_name_exact_multiplier),
analyzer=analysis.IDTokenizer() | analysis.LowercaseFilter(),
),
# The owner/repo/tool_id parsed from the GUID
"stub": KEYWORD(field_boost=float(config.tool_stub_boost)),
# The section where the tool is listed in the tool panel
"section": TEXT(field_boost=float(config.tool_section_boost)),
# Short description defined in the tool XML
"description": TEXT(
field_boost=config.tool_description_boost,
analyzer=analysis.StemmingAnalyzer(),
),
# Help text parsed from the tool XML
"help": TEXT(field_boost=config.tool_help_boost, analyzer=analysis.StemmingAnalyzer()),
"labels": KEYWORD(field_boost=float(config.tool_label_boost)),
}
if config.tool_enable_ngram_search:
schema_conf.update(
{
"name": NGRAMWORDS(
minsize=config.tool_ngram_minsize,
maxsize=config.tool_ngram_maxsize,
field_boost=(float(config.tool_name_boost) * config.tool_ngram_factor),
),
}
)
else:
schema_conf.update(
{
"name": TEXT(
field_boost=float(config.tool_name_boost),
),
}
)
self.schema = Schema(**schema_conf)
self.rex = analysis.RegexTokenizer()
self.index_dir = index_dir
self.panel_view_id = panel_view_id
self.index = self._index_setup()
def _index_setup(self) -> index.Index:
return get_or_create_index(index_dir=self.index_dir, schema=self.schema)
"""Get or create a reference to the index."""
return get_or_create_index(self.index_dir, self.schema)
def build_index(self, tool_cache, toolbox, 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.
"""Prepare search index for tools loaded in toolbox.
Use `tool_cache` to determine which tools need indexing and which
should be removed.
"""
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
indexed_tool_ids = {f["id"] for f in reader.all_stored_fields() if f}
tool_ids_to_remove = (indexed_tool_ids - set(tool_cache._tool_paths_by_id.keys())).union(
self.indexed_tool_ids = {f["id"] for f in reader.all_stored_fields() if f}
tool_ids_to_remove = self._get_tools_to_remove(tool_cache)
tools_to_index = self._get_tool_list(
toolbox,
tool_cache,
)
with AsyncWriter(self.index) as writer:
for tool_id in tool_ids_to_remove:
writer.delete_by_term("id", tool_id)
for tool in tools_to_index:
add_doc_kwds = self._create_doc(
tool=tool,
index_help=index_help,
)
# Add tool document to index (or overwrite if existing)
writer.update_document(**add_doc_kwds)
log.debug(f"Toolbox index of panel {self.panel_view_id}" f" finished {execution_timer}")
def _get_tools_to_remove(self, tool_cache) -> list:
"""Return list of tool IDs to be removed from index."""
tool_ids_to_remove = (self.indexed_tool_ids - set(tool_cache._tool_paths_by_id.keys())).union(
tool_cache._removed_tool_ids
)
for indexed_tool_id in indexed_tool_ids:
for indexed_tool_id in self.indexed_tool_ids:
indexed_tool = tool_cache.get_tool_by_id(indexed_tool_id)
if indexed_tool:
if indexed_tool.is_latest_version:
@@ -136,48 +238,58 @@ class ToolPanelViewSearch:
if latest_version and latest_version.hidden:
continue
tool_ids_to_remove.add(indexed_tool_id)
with AsyncWriter(self.index) as writer:
for tool_id in tool_ids_to_remove:
writer.delete_by_term("id", tool_id)
for tool_id in tool_cache._new_tool_ids - indexed_tool_ids:
tool = toolbox.get_tool(tool_id)
if tool and tool.is_latest_version and 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:
for tool_version in reversed(tool.lineage.get_versions()):
tool = tool_cache.get_tool_by_id(tool_version.id)
if tool and not tool.hidden:
tool_id = tool.id
break
else:
continue
else:
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(f"Toolbox index of panel {self.panel_view_id} finished {execution_timer}")
def _create_doc(self, tool_id: str, tool, index_help: bool = True) -> Dict[str, str]:
# Do not add data managers to the public index
return list(tool_ids_to_remove)
def _get_tool_list(self, toolbox, tool_cache) -> list:
"""Return list of tools to add and remove from index."""
tools_to_index = []
for tool_id in tool_cache._new_tool_ids - self.indexed_tool_ids:
tool = toolbox.get_tool(tool_id)
if tool and tool.is_latest_version and toolbox.panel_has_tool(tool, self.panel_view_id):
if tool.hidden:
# Check if there is an older tool we can return
if tool.lineage:
tool_versions = reversed(tool.lineage.get_versions())
for tool_version in tool_versions:
tool = tool_cache.get_tool_by_id(tool_version.id)
if tool and not tool.hidden:
break
else:
continue
tools_to_index.append(tool)
return tools_to_index
def _create_doc(
self,
tool,
index_help: bool = True,
) -> Dict[str, str]:
def clean(string):
"""Remove hyphens as they are Whoosh wildcards."""
if "-" in string:
return (" ").join(token.text for token in self.rex(to_unicode(tool.name)))
else:
return string
if tool.tool_type == "manage_data":
# Do not add data managers to the public index
return {}
add_doc_kwds = {
"id": tool_id,
"id": to_unicode(tool.id),
"id_exact": to_unicode(tool.id),
"name": clean(tool.name),
"description": to_unicode(tool.description),
"section": to_unicode(tool.get_panel_section()[1] if len(tool.get_panel_section()) == 2 else ""),
"help": to_unicode(""),
}
if tool.name.find("-") != -1:
# Replace hyphens, since they are wildcards in Whoosh causing false positives
add_doc_kwds["name"] = (" ").join(token.text for token in self.rex(to_unicode(tool.name)))
else:
add_doc_kwds["name"] = to_unicode(tool.name)
if tool.guid:
# Create a stub consisting of owner, repo, and tool from guid
slash_indexes = [m.start() for m in re.finditer("/", tool.guid)]
id_stub = tool.guid[(slash_indexes[1] + 1) : slash_indexes[4]]
add_doc_kwds["stub"] = (" ").join(token.text for token in self.rex(to_unicode(id_stub)))
add_doc_kwds["stub"] = clean(id_stub)
else:
add_doc_kwds["stub"] = to_unicode(id)
if tool.labels:
@@ -188,100 +300,49 @@ class ToolPanelViewSearch:
try:
add_doc_kwds["help"] = to_unicode(raw_help)
except Exception:
# Don't fail to build index just because help can't be converted.
# Don't fail to build index when help fails to parse
pass
add_doc_kwds["name_exact"] = add_doc_kwds["name"]
return add_doc_kwds
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,
config: GalaxyAppConfiguration,
) -> List[str]:
"""
Perform search on the in-memory index. Weight in the given boosts.
"""
"""Perform search on the in-memory index."""
# Change field boosts for searcher
self.searcher = self.index.searcher(
weighting=MultiWeighting(
BM25F(),
old_id=BM25F(old_id_B=float(tool_id_boost)),
name=BM25F(name_B=float(tool_name_boost)),
section=BM25F(section_B=float(tool_section_boost)),
description=BM25F(description_B=float(tool_description_boost)),
labels=BM25F(labels_B=float(tool_label_boost)),
stub=BM25F(stub_B=float(tool_stub_boost)),
help=BM25F(help_B=float(tool_help_boost)),
Frequency(),
help=BM25F(K1=config.tool_help_bm25f_k1),
)
)
# Use OrGroup to change the default operation for joining multiple terms to logical OR.
# This means e.g. for search 'bowtie of king arthur' a document that only has 'bowtie' will be a match.
# https://whoosh.readthedocs.io/en/latest/api/qparser.html#whoosh.qparser.MultifieldPlugin
# However this changes scoring i.e. searching 'bowtie of king arthur' a document with 'arthur arthur arthur'
# would have a higher score than a document with 'bowtie arthur' which is usually unexpected for a user.
# Hence we introduce a bonus on multi-hits using the 'factory()' method using a scaling factor between 0-1.
# https://whoosh.readthedocs.io/en/latest/parsing.html#searching-for-any-terms-instead-of-all-terms-by-default
# Adding the FuzzyTermPlugin to account for misspellings and typos, using a max distance of 2
og = OrGroup.factory(0.9)
fields = [
"id",
"id_exact",
"name",
"name_exact",
"description",
"section",
"help",
"labels",
"stub",
]
self.parser = MultifieldParser(
["name", "old_id", "description", "section", "help", "labels", "stub"], schema=self.schema, group=og
fields,
schema=self.schema,
group=OrGroup, # We need OR grouping to match StopList phrases
)
cleaned_query = " ".join(token.text for token in self.rex(q.lower()))
parsed_query = self.parser.parse(cleaned_query)
hits = self.searcher.search(
parsed_query,
limit=float(config.tool_search_limit),
sortedby="",
terms=True,
)
cleaned_query = q.lower()
if tool_enable_ngram_search is True:
rval = self._search_ngrams(cleaned_query, tool_ngram_minsize, tool_ngram_maxsize, tool_search_limit)
return rval
else:
cleaned_query = " ".join(token.text for token in self.rex(cleaned_query))
# Use asterisk Whoosh wildcard so e.g. 'bow' easily matches 'bowtie'
parsed_query = self.parser.parse(f"*{cleaned_query}*")
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: 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: 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:
# Get the tool list with respective scores for each qgram
curr_hits = self.searcher.search(self.parser.parse(f"*{query}*"), limit=float(tool_search_limit))
for i, curr_hit in enumerate(curr_hits):
is_present = False
for prev_hit in hits_with_score:
# Check if the tool appears again for the next qgram search
if curr_hit["id"] == prev_hit:
is_present = True
# Add the current score with the previous one if the
# tool appears again for the next qgram
hits_with_score[prev_hit] = curr_hits.score(i) + hits_with_score[prev_hit]
# Add the tool if not present to the collection with its score
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_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_list[0 : int(tool_search_limit)]]
return [hit["id"] for hit in hits[: config.tool_search_limit]]
@@ -27,6 +27,7 @@ from starlette.responses import (
)
from galaxy import util
from galaxy.exceptions import RequestParameterInvalidException
from galaxy.managers.context import ProvidesHistoryContext
from galaxy.schema import (
FilterQueryParams,
@@ -252,6 +253,8 @@ def parse_dataset_details(details: Optional[str]):
dataset_details: Optional[DatasetDetailsType] = None
if details and details != "all":
dataset_details = set(util.listify(details))
if "" in dataset_details:
raise RequestParameterInvalidException("Invalid empty IDs found in dataset details parameter")
else: # either None or 'all'
dataset_details = details # type: ignore
return dataset_details
@@ -357,7 +357,9 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin,
trans.sa_session.flush()
rval = trans.security.encode_all_ids(library_dataset.to_dict())
nice_size = util.nice_size(int(library_dataset.library_dataset_dataset_association.get_size()))
nice_size = util.nice_size(
int(library_dataset.library_dataset_dataset_association.get_size(calculate_size=False))
)
rval["file_size"] = nice_size
rval["update_time"] = library_dataset.update_time.strftime("%Y-%m-%d %I:%M %p")
rval["deleted"] = library_dataset.deleted
+46 -41
View File
@@ -393,27 +393,34 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController
email = user.email
username = user.username
inputs = list()
inputs.append(
{
"id": "email_input",
"name": "email",
"type": "text",
"label": "Email address",
"value": email,
"help": "If you change your email address you will receive an activation link in the new mailbox and you have to activate your account by visiting it.",
}
)
if trans.webapp.name == "galaxy":
user_info = {
"email": email,
"username": username,
}
is_galaxy_app = trans.webapp.name == "galaxy"
if trans.app.config.enable_account_interface or not is_galaxy_app:
inputs.append(
{
"id": "name_input",
"name": "username",
"id": "email_input",
"name": "email",
"type": "text",
"label": "Public name",
"value": username,
"help": 'Your public name is an identifier that will be used to generate addresses for information you share publicly. Public names must be at least three characters in length and contain only lower-case letters, numbers, and the "-" character.',
"label": "Email address",
"value": email,
"help": "If you change your email address you will receive an activation link in the new mailbox and you have to activate your account by visiting it.",
}
)
if is_galaxy_app:
if trans.app.config.enable_account_interface:
inputs.append(
{
"id": "name_input",
"name": "username",
"type": "text",
"label": "Public name",
"value": username,
"help": 'Your public name is an identifier that will be used to generate addresses for information you share publicly. Public names must be at least three characters in length and contain only lower-case letters, numbers, and the "-" character.',
}
)
info_form_models = self.get_all_forms(
trans, filter=dict(deleted=False), form_type=trans.app.model.FormDefinition.types.USER_INFO
)
@@ -441,25 +448,27 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController
info_field["cases"].append({"value": info_form["id"], "inputs": info_form["inputs"]})
inputs.append(info_field)
address_inputs = [{"type": "hidden", "name": "id", "hidden": True}]
for field in AddressField.fields():
address_inputs.append({"type": "text", "name": field[0], "label": field[1], "help": field[2]})
address_repeat = {
"title": "Address",
"name": "address",
"type": "repeat",
"inputs": address_inputs,
"cache": [],
}
address_values = [address.to_dict(trans) for address in user.addresses]
for address in address_values:
address_cache = []
for input in address_inputs:
input_copy = input.copy()
input_copy["value"] = address.get(input["name"])
address_cache.append(input_copy)
address_repeat["cache"].append(address_cache)
inputs.append(address_repeat)
if trans.app.config.enable_account_interface:
address_inputs = [{"type": "hidden", "name": "id", "hidden": True}]
for field in AddressField.fields():
address_inputs.append({"type": "text", "name": field[0], "label": field[1], "help": field[2]})
address_repeat = {
"title": "Address",
"name": "address",
"type": "repeat",
"inputs": address_inputs,
"cache": [],
}
address_values = [address.to_dict(trans) for address in user.addresses]
for address in address_values:
address_cache = []
for input in address_inputs:
input_copy = input.copy()
input_copy["value"] = address.get(input["name"])
address_cache.append(input_copy)
address_repeat["cache"].append(address_cache)
inputs.append(address_repeat)
user_info["addresses"] = [address.to_dict(trans) for address in user.addresses]
# Build input sections for extra user preferences
extra_user_pref = self._build_extra_user_pref_inputs(trans, self._get_extra_user_preferences(trans), user)
@@ -488,12 +497,8 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController
help='Your public name provides a means of identifying you publicly within this tool shed. Public names must be at least three characters in length and contain only lower-case letters, numbers, and the "-" character. You cannot change your public name after you have created a repository in this tool shed.',
)
)
return {
"email": email,
"username": username,
"addresses": [address.to_dict(trans) for address in user.addresses],
"inputs": inputs,
}
user_info["inputs"] = inputs
return user_info
@expose_api
def set_information(self, trans, id, payload=None, **kwd):
+1 -22
View File
@@ -244,32 +244,11 @@ class ToolsService(ServiceBase):
:return type: dict
"""
panel_view = view or self.config.default_panel_view
tool_name_boost = self.config.get("tool_name_boost", 9)
tool_id_boost = self.config.get("tool_id_boost", 9)
tool_section_boost = self.config.get("tool_section_boost", 3)
tool_description_boost = self.config.get("tool_description_boost", 2)
tool_label_boost = self.config.get("tool_label_boost", 1)
tool_stub_boost = self.config.get("tool_stub_boost", 5)
tool_help_boost = self.config.get("tool_help_boost", 0.5)
tool_search_limit = self.config.get("tool_search_limit", 20)
tool_enable_ngram_search = self.config.get("tool_enable_ngram_search", False)
tool_ngram_minsize = self.config.get("tool_ngram_minsize", 3)
tool_ngram_maxsize = self.config.get("tool_ngram_maxsize", 4)
results = self.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,
tool_description_boost=tool_description_boost,
tool_label_boost=tool_label_boost,
tool_stub_boost=tool_stub_boost,
tool_help_boost=tool_help_boost,
tool_search_limit=tool_search_limit,
tool_enable_ngram_search=tool_enable_ngram_search,
tool_ngram_minsize=tool_ngram_minsize,
tool_ngram_maxsize=tool_ngram_maxsize,
config=self.config,
)
return results
@@ -164,6 +164,14 @@ class HistoryContentsApiTestCase(ApiTestCase):
hda_details = self.__check_for_hda(contents_response, hda1)
self.__assert_hda_has_full_details(hda_details)
def test_index_detail_parameter_error(self):
hda1 = self.dataset_populator.new_dataset(self.history_id)
wrong_details_query_with_empty_ids = f"details=,,{hda1['id']}"
contents_response = self._get(
f"histories/{self.history_id}/contents?v=dev&{wrong_details_query_with_empty_ids}"
)
self._assert_status_code_is(contents_response, 400)
def test_show_hda(self):
hda1 = self.dataset_populator.new_dataset(self.history_id)
show_response = self.__show(hda1)
+1
View File
@@ -55,6 +55,7 @@ console_scripts =
[options.extras_require]
cwl =
cwltool==3.1.20211107152837
schema-salad==8.3.20220525163636
mulled =
jinja2
Whoosh
+2 -7
View File
@@ -124,14 +124,9 @@ if [ $SET_VENV -eq 1 ] && [ $CREATE_VENV -eq 1 ]; then
python3 -m venv "$GALAXY_VIRTUAL_ENV"
else
# If $GALAXY_VIRTUAL_ENV does not exist, and there is no conda available, attempt to create it.
if [ -z "$GALAXY_PYTHON" ]; then
if command -v python3 >/dev/null; then
GALAXY_PYTHON=python3
else
GALAXY_PYTHON=python
fi
fi
# Ensure Python is a supported version before creating $GALAXY_VIRTUAL_ENV
find_python_command
"$GALAXY_PYTHON" ./scripts/check_python.py || exit 1
echo "Creating Python virtual environment for Galaxy: $GALAXY_VIRTUAL_ENV"
echo "using Python: $GALAXY_PYTHON"
+12 -1
View File
@@ -90,6 +90,16 @@ conda_activate() {
fi
}
find_python_command() {
if [ -z "$GALAXY_PYTHON" ]; then
if command -v python3 >/dev/null; then
GALAXY_PYTHON=python3
else
GALAXY_PYTHON=python
fi
fi
}
setup_python() {
# If there is a .venv/ directory, assume it contains a virtualenv that we
# should run this instance in.
@@ -120,7 +130,8 @@ setup_python() {
# in case you don't.
[ -n "$PYTHONPATH" ] && echo 'WARNING: $PYTHONPATH is set, this can cause problems importing Galaxy dependencies'
python ./scripts/check_python.py || exit 1
find_python_command
"$GALAXY_PYTHON" ./scripts/check_python.py || exit 1
}
setup_gravity_state_dir() {
+3
View File
@@ -25,12 +25,15 @@ sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pa
from galaxy.model.migrations.scripts import (
invoke_alembic,
LegacyScripts,
verify_database_is_initialized,
)
def run():
ls = LegacyScripts(sys.argv, os.getcwd())
ls.run()
db_url = ls.get_db_url()
verify_database_is_initialized(db_url)
invoke_alembic()
@@ -1,19 +1,30 @@
<tool id="options_from_metadata_file" name="Test for options from dataset metadata file" version="1.0.0" profile="21.01">
<command><![CDATA[
echo '${species}' > '${output}';
echo '${species_comma}' >> '${output}';
echo '${species_2}' >> '${output}'
]]></command>
<inputs>
<param name="input" type="data" format="maf" label="MAF File"/>
<!-- test meta_file_key to define options-->
<param name="species" type="select" optional="false" label="Select species for the input dataset" multiple="true">
<options from_dataset="input" meta_file_key="species_chromosomes" >
<column name="name" index="0"/>
<column name="value" index="0"/>
<options from_dataset="input" meta_file_key="species_chromosomes">
<column name="name" index="0"/>
<column name="value" index="0"/>
</options>
</param>
<param name="input_2" type="data_collection" collection_type="list" format="maf" label="MAF Collection" multiple="true" />
<!-- test meta_file_key in combination with separator to define options
(since the metadata file does not contain commas each line gets an option)-->
<param name="species_comma" type="select" optional="false" label="Select species for the input dataset">
<options from_dataset="input" meta_file_key="species_chromosomes" separator=",">
<column name="name" index="0"/>
<column name="value" index="0"/>
</options>
</param>
<!-- test meta_file_key referring a collection to define options-->
<param name="input_2" type="data_collection" collection_type="list" format="maf" label="MAF Collection" multiple="true"/>
<param name="species_2" type="select" optional="false" label="Select species for the input dataset" multiple="true">
<options from_dataset="input_2" meta_file_key="species_chromosomes" >
<options from_dataset="input_2" meta_file_key="species_chromosomes">
<column name="name" index="0"/>
<column name="value" index="0"/>
<filter type="unique_value" name="unique_param" column="0"/>
@@ -21,23 +32,25 @@ echo '${species_2}' >> '${output}'
</param>
</inputs>
<outputs>
<data format="txt" name="output" />
<data format="txt" name="output"/>
</outputs>
<tests>
<test>
<param name="input" value="3.maf" ftype="maf" />
<param name="species" value="hg17,canFam1" />
<param name="input" value="3.maf" ftype="maf"/>
<param name="species" value="hg17,canFam1"/>
<param name="species_comma" value="hg17&#009;chr7"/>
<param name="input_2">
<collection type="list">
<element name="e1" value="3.maf" />
<element name="e2" value="4.maf" />
<element name="e1" value="3.maf"/>
<element name="e2" value="4.maf"/>
</collection>
</param>
<param name="species_2" value="panTro1,rn3,bosTau2" />
<param name="species_2" value="panTro1,rn3,bosTau2"/>
<output name="output" ftype="txt">
<assert_contents>
<has_line line="hg17,canFam1" />
<has_line line="panTro1,rn3,bosTau2" />
<has_line line="hg17,canFam1"/>
<has_line line="hg17__tc__chr7"/>
<has_line line="panTro1,rn3,bosTau2"/>
</assert_contents>
</output>
</test>
@@ -53,6 +53,7 @@
<tool file="filter_param_value_ref_attribute.xml" />
<tool file="filter_static_regexp.xml" />
<tool file="select_from_dataset.xml" />
<tool file="select_from_csvdataset.xml" />
<tool file="select_from_dataset_optional.xml" />
<tool file="drill_down.xml" />
<tool file="dbkey_filter_input.xml" />
@@ -0,0 +1,40 @@
<tool id="select_from_csvdataset" name="select_from_csvdataset" version="0.1.0">
<description>Create dynamic options from CSV data sets</description>
<command><![CDATA[
echo select_single '$select_single' > '$output'
]]></command>
<inputs>
<param name="single" type="data" format="csv" label="single"/>
<param name="select_single" type="select" label="select_single">
<options from_dataset="single" separator="," startswith="Transaction_date">
<column name="name" index="1"/>
<column name="value" index="2"/>
<validator type="no_options" message="No data is available in single" />
</options>
</param>
</inputs>
<outputs>
<data name="output" format="txt" />
</outputs>
<tests>
<!-- test that csv can be selected, i.e. if the separator attribute of <options> works-->
<test>
<param name="single" value="1.csv" />
<param name="select_single" value="Product1" />
<output name="output">
<assert_contents>
<has_text text="select_single 1200" />
</assert_contents>
</output>
</test>
<!-- test that data from "comment" lines can not be selected, i.e. if the startswith attribute of <options> works-->
<test expect_failure="true">
<param name="single" value="1.csv" />
<param name="select_single" value="Product" />
</test>
</tests>
<help>
</help>
</tool>
@@ -1,8 +1,13 @@
import random
import pytest
from galaxy.model.migrations.scripts import (
DatabaseDoesNotExistError,
DatabaseNotInitializedError,
LegacyScripts,
LegacyScriptsException,
verify_database_is_initialized,
)
@@ -145,3 +150,25 @@ class TestLegacyScripts:
argv = ["caller", "--alembic-config", "path-to-alembic", "downgrade"]
with pytest.raises(LegacyScriptsException):
LegacyScripts(argv).convert_args()
def test_access_database_id(self):
db = "galaxy"
argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade", db]
ls = LegacyScripts(argv)
ls.run()
assert ls.database == db
def test_access_database_id_before_processing_script_args_raises_error(self):
argv = ["caller", "--alembic-config", "path-to-alembic", "upgrade"]
with pytest.raises(LegacyScriptsException):
LegacyScripts(argv).database
def test_verify_database_is_init_raises_error_if_no_database(self):
nonexistant_path = str(random.random())[2:]
db_url = f"sqlite:////{nonexistant_path}"
with pytest.raises(DatabaseDoesNotExistError):
verify_database_is_initialized(db_url)
def test_verify_database_is_init_raises_error_if_database_not_initialized(self, sqlite_memory_url):
with pytest.raises(DatabaseNotInitializedError):
verify_database_is_initialized(sqlite_memory_url)