mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #15210 from ahmedhamidawan/filter_inputs_in_history
Filter Inputs and Outputs in HistoryPanel
This commit is contained in:
@@ -14,27 +14,34 @@
|
||||
<icon v-if="selected" fixed-width size="lg" :icon="['far', 'check-square']" />
|
||||
<icon v-else fixed-width size="lg" :icon="['far', 'square']" />
|
||||
</b-button>
|
||||
<span v-if="highlight == 'input'" v-b-tooltip.hover title="Input" @click.stop="toggleHighlights">
|
||||
<b-button
|
||||
v-if="highlight == 'input'"
|
||||
v-b-tooltip.hover
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Input"
|
||||
@click.stop="toggleHighlights">
|
||||
<font-awesome-icon class="text-info" icon="arrow-circle-up" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="highlight == 'noInputs'"
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="highlight == 'active'"
|
||||
v-b-tooltip.hover
|
||||
title="No Inputs for this item"
|
||||
tabindex="0"
|
||||
@click.stop="toggleHighlights"
|
||||
@keypress="toggleHighlights">
|
||||
<font-awesome-icon icon="minus-circle" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="highlight == 'output'"
|
||||
v-b-tooltip.hover
|
||||
title="Inputs highlighted for this item"
|
||||
tabindex="0"
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Inputs/Outputs highlighted for this item"
|
||||
@click.stop="toggleHighlights"
|
||||
@keypress="toggleHighlights">
|
||||
<font-awesome-icon icon="check-circle" />
|
||||
</span>
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="highlight == 'output'"
|
||||
v-b-tooltip.hover
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Output"
|
||||
@click.stop="toggleHighlights">
|
||||
<font-awesome-icon class="text-info" icon="arrow-circle-down" />
|
||||
</b-button>
|
||||
<span v-if="hasStateIcon" class="state-icon">
|
||||
<icon fixed-width :icon="contentState.icon" :spin="contentState.spin" />
|
||||
</span>
|
||||
@@ -86,7 +93,7 @@
|
||||
v-if="expandDataset"
|
||||
:dataset="item"
|
||||
:writable="writable"
|
||||
:show-highlight="isHistoryItem"
|
||||
:show-highlight="isHistoryItem && filterable"
|
||||
:item-urls="itemUrls"
|
||||
@edit="onEdit"
|
||||
@toggleHighlights="toggleHighlights" />
|
||||
@@ -104,11 +111,10 @@ import { updateContentFields } from "components/History/model/queries";
|
||||
import { JobStateSummary } from "./Collection/JobStateSummary";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faArrowCircleUp, faMinusCircle, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowCircleUp, faArrowCircleDown, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useEntryPointStore } from "stores/entryPointStore";
|
||||
|
||||
library.add(faArrowCircleUp, faMinusCircle, faCheckCircle);
|
||||
|
||||
library.add(faArrowCircleUp, faArrowCircleDown, faCheckCircle);
|
||||
export default {
|
||||
components: {
|
||||
CollectionDescription,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Specifies highlighted items in the history listing. The `highlight` property is passed to
|
||||
* the content item component and can be used to modify its appearance.
|
||||
* TO DO: Consider case where parameter history is different and hence inputs cannot be seen
|
||||
* in the current panel.
|
||||
*/
|
||||
import axios from "axios";
|
||||
import { prependPath } from "utils/redirect";
|
||||
import { deepeach } from "utils/utils";
|
||||
import { LastQueue } from "utils/promise-queue";
|
||||
|
||||
// add promise queue
|
||||
const lastQueue = new LastQueue(300);
|
||||
|
||||
/** Local cache for parameter requests */
|
||||
const paramStash = new Map();
|
||||
|
||||
/** Performs request to obtain dataset parameters */
|
||||
async function getDatasetParameters(datasetId, jobId) {
|
||||
if (!paramStash.has(datasetId)) {
|
||||
const url = jobId
|
||||
? `api/jobs/${jobId}/parameters_display`
|
||||
: `api/datasets/${datasetId}/parameters_display?hda_ldda=hda`;
|
||||
const { data } = await lastQueue.enqueue(axios.get, prependPath(url));
|
||||
paramStash.set(datasetId, data);
|
||||
}
|
||||
return paramStash.get(datasetId);
|
||||
}
|
||||
|
||||
/** Returns item key */
|
||||
function getKey(details) {
|
||||
if (details.id && details.src) {
|
||||
const historyContentType = details.src == "hda" ? "dataset" : "dataset_collection";
|
||||
return `${details.id}-${historyContentType}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns highlighting details */
|
||||
export async function getHighlights(item, itemKey) {
|
||||
const highlights = {};
|
||||
const { outputs, parameters } = await getDatasetParameters(item.id, item.job_source_id);
|
||||
deepeach(parameters, (details) => {
|
||||
const key = getKey(details);
|
||||
if (key) {
|
||||
highlights[key] = "input";
|
||||
}
|
||||
});
|
||||
deepeach(outputs, (details) => {
|
||||
const key = getKey(details);
|
||||
if (key) {
|
||||
// some other item created this item (e.g.: inheritance)
|
||||
if (key != itemKey) {
|
||||
highlights[itemKey] = "output";
|
||||
highlights[key] = "input";
|
||||
} else {
|
||||
highlights[key] = "output";
|
||||
}
|
||||
}
|
||||
});
|
||||
// highlights only has item itself as an output (i.e.: no inputs)
|
||||
if (highlights[itemKey] === "output" && Object.keys(highlights).length == 1) {
|
||||
highlights[itemKey] = "noInputs";
|
||||
}
|
||||
// TO DO: Consider case where a job created multiple items (all highlights are outputs)
|
||||
return highlights;
|
||||
}
|
||||
@@ -46,6 +46,8 @@
|
||||
<small class="mt-1">Filter by state:</small>
|
||||
<b-form-input v-model="filterSettings['state:']" size="sm" placeholder="any state" list="stateSelect" />
|
||||
<b-form-datalist id="stateSelect" :options="states"></b-form-datalist>
|
||||
<small class="mt-1">Filter by related to item index:</small>
|
||||
<b-form-input v-model="filterSettings['related:']" size="sm" placeholder="index equals" />
|
||||
<small class="mt-1">Filter by item index:</small>
|
||||
<b-form-group class="m-0">
|
||||
<b-input-group>
|
||||
|
||||
@@ -144,7 +144,6 @@ import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ContentItem from "components/History/Content/ContentItem";
|
||||
import { deleteContent, updateContentFields } from "components/History/model/queries";
|
||||
import { getHighlights } from "components/History/Content/model/highlights";
|
||||
import ExpandedItems from "components/History/Content/ExpandedItems";
|
||||
import SelectedItems from "components/History/Content/SelectedItems";
|
||||
import ListingLayout from "components/History/Layout/ListingLayout";
|
||||
@@ -193,7 +192,6 @@ export default {
|
||||
return {
|
||||
error: null,
|
||||
filterText: "",
|
||||
highlights: {},
|
||||
highlightsKey: null,
|
||||
invisible: {},
|
||||
loading: false,
|
||||
@@ -255,13 +253,11 @@ export default {
|
||||
queryKey() {
|
||||
this.invisible = {};
|
||||
this.offset = 0;
|
||||
this.resetHighlights();
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
historyId(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.operationRunning = null;
|
||||
this.resetHighlights();
|
||||
}
|
||||
},
|
||||
filter(newVal) {
|
||||
@@ -281,10 +277,19 @@ export default {
|
||||
...vuexMapActions("history", ["loadHistoryById"]),
|
||||
...mapActions(useHistoryItemsStore, ["fetchHistoryItems"]),
|
||||
getHighlight(item) {
|
||||
return this.highlights[this.getItemKey(item)];
|
||||
},
|
||||
getItemKey(item) {
|
||||
return `${item.id}-${item.history_content_type}`;
|
||||
if (this.filterText.includes("related:" + item.hid)) {
|
||||
this.highlightsKey = item.hid;
|
||||
return "active";
|
||||
} else if (this.filterText.includes("related:") && this.highlightsKey) {
|
||||
if (item.hid > this.highlightsKey) {
|
||||
return "output";
|
||||
} else {
|
||||
return "input";
|
||||
}
|
||||
} else {
|
||||
this.highlightsKey = null;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
hasMatches(items) {
|
||||
return !!items && items.length > 0;
|
||||
@@ -344,19 +349,13 @@ export default {
|
||||
console.debug("HistoryPanel - Operation error.", error);
|
||||
this.operationError = error;
|
||||
},
|
||||
async toggleHighlights(item) {
|
||||
const key = this.getItemKey(item);
|
||||
if (this.highlightsKey != key) {
|
||||
this.highlightsKey = key;
|
||||
this.highlights = await getHighlights(item, key);
|
||||
toggleHighlights(item) {
|
||||
if (this.filterText == "related:" + item.hid) {
|
||||
this.filterText = "";
|
||||
} else {
|
||||
this.resetHighlights();
|
||||
this.filterText = "related:" + item.hid;
|
||||
}
|
||||
},
|
||||
resetHighlights() {
|
||||
this.highlights = {};
|
||||
this.highlightsKey = null;
|
||||
},
|
||||
onDragEnter(e) {
|
||||
this.dragTarget = e.target;
|
||||
this.showDropZone = true;
|
||||
|
||||
@@ -9,6 +9,7 @@ export const validFilters = {
|
||||
hid_gt: compare("hid", "gt"),
|
||||
hid_le: compare("hid", "le"),
|
||||
hid_lt: compare("hid", "lt"),
|
||||
related: equals("related"),
|
||||
tag: contains("tags", "tag", expandNameTag),
|
||||
visible: equals("visible", "visible", toBool),
|
||||
deleted: equals("deleted", "deleted", toBool),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import Vue from "vue";
|
||||
import { reverse } from "lodash";
|
||||
import { LastQueue } from "utils/promise-queue";
|
||||
import { urlData } from "utils/url";
|
||||
@@ -21,13 +21,15 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
|
||||
latestCreateTime: new Date(),
|
||||
totalMatchesCount: undefined,
|
||||
lastCheckedTime: new Date(),
|
||||
relatedItems: {},
|
||||
isWatching: false,
|
||||
}),
|
||||
getters: {
|
||||
getHistoryItems: (state) => {
|
||||
return (historyId, filterText) => {
|
||||
const itemArray = state.items[historyId] || [];
|
||||
const filters = HistoryFilters.getFilters(filterText);
|
||||
const filters = HistoryFilters.getFilters(filterText).filter((filter) => !filter.includes("related"));
|
||||
const relatedHid = HistoryFilters.getFilterValue(filterText, "related");
|
||||
const filtered = itemArray.filter((item) => {
|
||||
if (!item) {
|
||||
return false;
|
||||
@@ -35,6 +37,10 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
|
||||
if (!HistoryFilters.testFilters(filters, item)) {
|
||||
return false;
|
||||
}
|
||||
const relationKey = `${historyId}-${relatedHid}-${item.hid}`;
|
||||
if (relatedHid && !state.relatedItems[relationKey]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return reverse(filtered);
|
||||
@@ -63,11 +69,12 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
|
||||
const stats = data.stats;
|
||||
this.totalMatchesCount = stats.total_matches;
|
||||
const payload = data.contents;
|
||||
this.saveHistoryItems(historyId, payload);
|
||||
const relatedHid = HistoryFilters.getFilterValue(filterText, "related");
|
||||
this.saveHistoryItems(historyId, payload, relatedHid);
|
||||
});
|
||||
},
|
||||
// Setters
|
||||
saveHistoryItems(historyId, payload) {
|
||||
saveHistoryItems(historyId, payload, relatedHid = null) {
|
||||
this.$patch((state) => {
|
||||
// merges incoming payload into existing state
|
||||
mergeArray(historyId, payload, state.items, state.itemKey);
|
||||
@@ -79,6 +86,10 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
|
||||
state.latestCreateTime = itemCreateTime;
|
||||
}
|
||||
}
|
||||
if (relatedHid) {
|
||||
const relationKey = `${historyId}-${relatedHid}-${item.hid}`;
|
||||
Vue.set(state.relatedItems, relationKey, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -29,12 +29,23 @@ describe("filtering", () => {
|
||||
expect(queryDict["name-contains"]).toBe("name of item");
|
||||
});
|
||||
test("parse check filter", () => {
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "name", "name of item")).toBe(true);
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "tag", "first")).toBe(true);
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "tag", "second")).toBe(false);
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "deleted", "false")).toBe(true);
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "visible", true)).toBe(true);
|
||||
expect(HistoryFilters.checkFilter(filterTexts[0], "visible", "false")).toBe(false);
|
||||
});
|
||||
test("parse get filter value", () => {
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "name")).toBe("name of item");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "hid", "gt")).toBe("10");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "hid", "lt")).toBe("100");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "tag")).toBe("first");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "deleted")).toBe("False");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[0], "visible")).toBe("TRUE");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[1], "hid", "gt")).toBe("10");
|
||||
expect(HistoryFilters.getFilterValue(filterTexts[1], "create_time", "gt")).toBe("2021-01-01");
|
||||
});
|
||||
test("parse filter text as entries", () => {
|
||||
filterTexts.forEach((filterText) => {
|
||||
const filters = HistoryFilters.getFilters(filterText);
|
||||
|
||||
@@ -18,6 +18,14 @@ const defaultValidAliases = [
|
||||
["<", "_lt"],
|
||||
];
|
||||
|
||||
const operatorForAlias: Record<string, string> = {
|
||||
lt: "<",
|
||||
le: "<=",
|
||||
ge: ">=",
|
||||
gt: ">",
|
||||
eq: ":",
|
||||
};
|
||||
|
||||
/** Converts user input to backend compatible date
|
||||
* @param {string} value
|
||||
* @returns {Number} seconds since epoch
|
||||
@@ -61,6 +69,14 @@ export function expandNameTag(value: string | object): string {
|
||||
return toLower(value);
|
||||
}
|
||||
|
||||
/** Converts string alias to string operator, e.g.: 'gt' to '>'
|
||||
* @param alias
|
||||
* @returns Arithmetic operator, e.g.: '>'
|
||||
* */
|
||||
export function getOperatorForAlias(alias: string): string {
|
||||
return operatorForAlias[alias];
|
||||
}
|
||||
|
||||
type HandlerReturn<T> = {
|
||||
attribute: string;
|
||||
converter?: Converter<T>;
|
||||
@@ -323,12 +339,28 @@ export default class Filtering<T> {
|
||||
* @returns {Boolean} True if the filter is set to the given value
|
||||
* */
|
||||
checkFilter<T>(filterText: string, filterName: string, filterValue: T): boolean {
|
||||
const re = new RegExp(`${filterName}:(\\S+)`);
|
||||
const reMatch = re.exec(filterText);
|
||||
const testValue = reMatch ? reMatch[1] : this.defaultFilters[filterName];
|
||||
const testValue = this.getFilterValue(filterText, filterName);
|
||||
return toLowerNoQuotes(testValue) === toLowerNoQuotes(filterValue);
|
||||
}
|
||||
|
||||
/** Get the value of a particular filter from filterText.
|
||||
* @param filterText Raw filter text string
|
||||
* @param filterName Filter key to check
|
||||
* @param [alias="eq"] String alias for filter operator, e.g.:"lt"
|
||||
* @returns The filterValue for the filter
|
||||
* */
|
||||
getFilterValue(filterText: string, filterName: string, alias = "eq"): string | boolean {
|
||||
const op = getOperatorForAlias(alias);
|
||||
const reString = `${filterName}(?:${op}|[-|_]${alias}:)(?:'([^']*[^\\s']*)'|(\\S+))`;
|
||||
const re = new RegExp(reString);
|
||||
const reMatch = re.exec(filterText);
|
||||
let filterVal = null;
|
||||
if (reMatch) {
|
||||
filterVal = reMatch[1] || reMatch[2];
|
||||
}
|
||||
return filterVal || this.defaultFilters[filterName];
|
||||
}
|
||||
|
||||
/** Test if an item passes all filters.
|
||||
* @param {Object} filters Parsed in key-value pairs from getFilters()
|
||||
* @param {Object} item Item to test against the filters
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Heterogenous lists/contents are difficult to query properly since unions are
|
||||
not easily made.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -41,6 +42,8 @@ from galaxy.managers import (
|
||||
taggable,
|
||||
tools,
|
||||
)
|
||||
from galaxy.managers.job_connections import JobConnectionsManager
|
||||
from galaxy.schema import ValueFilterQueryParams
|
||||
from galaxy.structured_app import MinimalManagerApp
|
||||
from .base import (
|
||||
parse_bool,
|
||||
@@ -525,6 +528,30 @@ class HistoryContentsFilters(
|
||||
# surprisingly (but ominously), this works for both content classes in the union that's filtered
|
||||
model_class = model.HistoryDatasetAssociation
|
||||
|
||||
def parse_query_filters_with_relations(self, query_filters: ValueFilterQueryParams, history_id):
|
||||
"""Parse query filters but consider case where related filter is included."""
|
||||
if query_filters.q and query_filters.qv and "related-eq" in query_filters.q:
|
||||
qv_index = query_filters.q.index("related-eq")
|
||||
qv_hid = query_filters.qv[qv_index]
|
||||
|
||||
# Make new q and qv excluding related filter
|
||||
new_q = [x for i, x in enumerate(query_filters.q) if i != qv_index]
|
||||
new_qv = [x for i, x in enumerate(query_filters.qv) if i != qv_index]
|
||||
|
||||
# Get list of related item hids from job_connections manager
|
||||
job_connections_manager = JobConnectionsManager(self.app.model.session)
|
||||
related = job_connections_manager.get_related_hids(history_id, qv_hid)
|
||||
|
||||
# Make new query_filters with updated list of related hids for given hid
|
||||
new_q.append("related-eq")
|
||||
new_qv.append(json.dumps(related))
|
||||
query_filters_with_relations = ValueFilterQueryParams(
|
||||
q=new_q,
|
||||
qv=new_qv,
|
||||
)
|
||||
return super().parse_query_filters(query_filters_with_relations)
|
||||
return super().parse_query_filters(query_filters)
|
||||
|
||||
def _parse_orm_filter(self, attr, op, val):
|
||||
|
||||
# we need to use some manual/text/column fu here since some where clauses on the union don't work
|
||||
@@ -538,6 +565,9 @@ class HistoryContentsFilters(
|
||||
return sql.column("history_content_type") == val
|
||||
raise_filter_err(attr, op, val, "bad op in filter")
|
||||
|
||||
if attr == "related" and op == "eq":
|
||||
return sql.column("hid").in_(json.loads(val))
|
||||
|
||||
if attr == "type_id":
|
||||
if op == "eq":
|
||||
return sql.column("type_id") == val
|
||||
@@ -595,6 +625,7 @@ class HistoryContentsFilters(
|
||||
self.orm_filter_parsers.update(
|
||||
{
|
||||
"history_content_type": {"op": ("eq")},
|
||||
"related": {"op": ("eq")},
|
||||
"type_id": {"op": ("eq", "in"), "val": self.parse_type_id_list},
|
||||
"hid": {"op": ("eq", "ge", "le", "gt", "lt"), "val": int},
|
||||
# TODO: needs a different val parser - but no way to add to the above
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
from sqlalchemy import (
|
||||
literal,
|
||||
union,
|
||||
)
|
||||
from sqlalchemy.sql import (
|
||||
expression,
|
||||
select,
|
||||
)
|
||||
|
||||
from galaxy import model
|
||||
from galaxy.managers.base import get_class
|
||||
from galaxy.model.scoped_session import galaxy_scoped_session
|
||||
|
||||
|
||||
class JobConnectionsManager:
|
||||
"""Get connections graph of inputs and outputs for given item"""
|
||||
|
||||
def __init__(self, sa_session: galaxy_scoped_session):
|
||||
self.sa_session = sa_session
|
||||
|
||||
def get_connections_graph(self, id, src):
|
||||
"""Get connections graph of inputs and outputs for given item id"""
|
||||
if src == "HistoryDatasetAssociation":
|
||||
output_selects = self.outputs_derived_from_input_hda(id)
|
||||
input_selects = self.inputs_for_hda(id)
|
||||
elif src == "HistoryDatasetCollectionAssociation":
|
||||
output_selects = self.outputs_derived_from_input_hdca(id)
|
||||
input_selects = self.inputs_for_hdca(id)
|
||||
else:
|
||||
raise Exception(f"Invalid item type {src}")
|
||||
# Execute selects and return graph of inputs and outputs
|
||||
result = {}
|
||||
result["outputs"] = self._get_union_results(*output_selects)
|
||||
result["inputs"] = self._get_union_results(*input_selects)
|
||||
return result
|
||||
|
||||
def get_related_hids(self, history_id, hid):
|
||||
"""Get connections graph of inputs and outputs for given item hid from the given history_id"""
|
||||
# Get id(s) and src(s) for the given hid
|
||||
items_by_hid = self.sa_session.execute(
|
||||
select(model.HistoryDatasetAssociation.id, expression.literal("HistoryDatasetAssociation"))
|
||||
.filter_by(history_id=history_id, hid=hid)
|
||||
.union(
|
||||
select(
|
||||
model.HistoryDatasetCollectionAssociation.id,
|
||||
expression.literal("HistoryDatasetCollectionAssociation"),
|
||||
).filter_by(history_id=history_id, hid=hid)
|
||||
)
|
||||
).all()
|
||||
result = [hid]
|
||||
for item_data in items_by_hid:
|
||||
graph = self.get_connections_graph(id=item_data[0], src=item_data[1])
|
||||
# Add found related items' hids to result list
|
||||
for val in graph["outputs"] + graph["inputs"]:
|
||||
item_class = get_class(val["src"])
|
||||
item = self.sa_session.query(item_class).get(val["id"])
|
||||
result.append(item.hid)
|
||||
return result
|
||||
|
||||
def _get_union_results(self, *selects):
|
||||
result = []
|
||||
for row in self.sa_session.execute(union(*selects)).all():
|
||||
result.append({"src": row.src, "id": row.id})
|
||||
return result
|
||||
|
||||
def outputs_derived_from_input_hda(self, input_hda_id):
|
||||
hda_select = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetAssociation").label("src"),
|
||||
model.JobToOutputDatasetAssociation.dataset_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToInputDatasetAssociation,
|
||||
model.JobToInputDatasetAssociation.job_id == model.JobToOutputDatasetAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToInputDatasetAssociation.dataset_id == input_hda_id)
|
||||
)
|
||||
hdca_select = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetCollectionAssociation").label("src"),
|
||||
model.JobToOutputDatasetCollectionAssociation.dataset_collection_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToInputDatasetAssociation,
|
||||
model.JobToInputDatasetAssociation.job_id == model.JobToOutputDatasetCollectionAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToInputDatasetAssociation.dataset_id == input_hda_id)
|
||||
)
|
||||
return hda_select, hdca_select
|
||||
|
||||
def outputs_derived_from_input_hdca(self, input_hdca_id):
|
||||
hda_select = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetAssociation").label("src"),
|
||||
model.JobToOutputDatasetAssociation.dataset_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToInputDatasetCollectionAssociation,
|
||||
model.JobToInputDatasetCollectionAssociation.job_id == model.JobToOutputDatasetAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToInputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
|
||||
)
|
||||
hdca_select = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetCollectionAssociation").label("src"),
|
||||
model.JobToOutputDatasetCollectionAssociation.dataset_collection_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToInputDatasetCollectionAssociation,
|
||||
model.JobToInputDatasetCollectionAssociation.job_id
|
||||
== model.JobToOutputDatasetCollectionAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToInputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
|
||||
)
|
||||
return hda_select, hdca_select
|
||||
|
||||
def inputs_for_hda(self, input_hda_id):
|
||||
input_hdas = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetAssociation").label("src"),
|
||||
model.JobToInputDatasetAssociation.dataset_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToOutputDatasetAssociation,
|
||||
model.JobToOutputDatasetAssociation.job_id == model.JobToInputDatasetAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToOutputDatasetAssociation.dataset_id == input_hda_id)
|
||||
)
|
||||
input_hdcas = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetCollectionAssociation").label("src"),
|
||||
model.JobToInputDatasetCollectionAssociation.dataset_collection_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToOutputDatasetAssociation,
|
||||
model.JobToOutputDatasetAssociation.job_id == model.JobToInputDatasetCollectionAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToOutputDatasetAssociation.dataset_id == input_hda_id)
|
||||
)
|
||||
return input_hdas, input_hdcas
|
||||
|
||||
def inputs_for_hdca(self, input_hdca_id):
|
||||
input_hdas = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetAssociation").label("src"),
|
||||
model.JobToInputDatasetAssociation.dataset_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToOutputDatasetCollectionAssociation,
|
||||
model.JobToOutputDatasetCollectionAssociation.job_id == model.JobToInputDatasetAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToOutputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
|
||||
)
|
||||
input_hdcas = (
|
||||
select(
|
||||
[
|
||||
literal("HistoryDatasetCollectionAssociation").label("src"),
|
||||
model.JobToInputDatasetCollectionAssociation.dataset_collection_id.label("id"),
|
||||
]
|
||||
)
|
||||
.join(
|
||||
model.JobToOutputDatasetCollectionAssociation,
|
||||
model.JobToOutputDatasetCollectionAssociation.job_id
|
||||
== model.JobToInputDatasetCollectionAssociation.job_id,
|
||||
)
|
||||
.where(model.JobToOutputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
|
||||
)
|
||||
return input_hdas, input_hdcas
|
||||
@@ -939,7 +939,8 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto
|
||||
Allows additional filtering of contents and custom serialization.
|
||||
"""
|
||||
history = self._get_history(trans, history_id)
|
||||
filters = self.history_contents_filters.parse_query_filters(filter_query_params)
|
||||
|
||||
filters = self.history_contents_filters.parse_query_filters_with_relations(filter_query_params, history_id)
|
||||
|
||||
stats_requested = accept == HistoryContentsWithStatsResult.__accept_type__
|
||||
if stats_requested and self.history_contents_filters.contains_non_orm_filter(filters):
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import pytest
|
||||
from sqlalchemy import union
|
||||
|
||||
from galaxy.managers.job_connections import JobConnectionsManager
|
||||
from galaxy.model import (
|
||||
HistoryDatasetAssociation,
|
||||
HistoryDatasetCollectionAssociation,
|
||||
Job,
|
||||
)
|
||||
from galaxy.model.scoped_session import galaxy_scoped_session
|
||||
from galaxy.model.unittest_utils import GalaxyDataTestApp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sa_session():
|
||||
return GalaxyDataTestApp().model.session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def job_connections_manager(sa_session) -> JobConnectionsManager:
|
||||
gm = JobConnectionsManager(sa_session)
|
||||
return gm
|
||||
|
||||
|
||||
# =============================================================================
|
||||
def setup_connected_dataset(sa_session: galaxy_scoped_session):
|
||||
center_hda = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
input_hda = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
input_hdca = HistoryDatasetCollectionAssociation()
|
||||
output_hda = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
output_hdca = HistoryDatasetCollectionAssociation()
|
||||
input_job = Job()
|
||||
output_job = Job()
|
||||
input_job.add_output_dataset("output_hda", center_hda)
|
||||
input_job.add_input_dataset("input_hda", input_hda)
|
||||
input_job.add_input_dataset_collection("input_hdca", input_hdca)
|
||||
output_job.add_input_dataset("input_hda", center_hda)
|
||||
output_job.add_output_dataset("output_hda", output_hda)
|
||||
output_job.add_output_dataset_collection("output_hdca", output_hdca)
|
||||
sa_session.add_all([center_hda, input_hda, input_hdca, output_hdca, input_job, output_job])
|
||||
sa_session.flush()
|
||||
expected_graph = {
|
||||
"inputs": [
|
||||
{"src": "HistoryDatasetAssociation", "id": input_hda.id},
|
||||
{"src": "HistoryDatasetCollectionAssociation", "id": input_hdca.id},
|
||||
],
|
||||
"outputs": [
|
||||
{"src": "HistoryDatasetAssociation", "id": output_hda.id},
|
||||
{"src": "HistoryDatasetCollectionAssociation", "id": output_hdca.id},
|
||||
],
|
||||
}
|
||||
return center_hda, expected_graph
|
||||
|
||||
|
||||
def setup_connected_dataset_collection(sa_session: galaxy_scoped_session):
|
||||
center_hdca = HistoryDatasetCollectionAssociation()
|
||||
input_hda1 = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
input_hda2 = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
input_hdca = HistoryDatasetCollectionAssociation()
|
||||
output_hda = HistoryDatasetAssociation(sa_session=sa_session)
|
||||
output_hdca = HistoryDatasetCollectionAssociation()
|
||||
input_job = Job()
|
||||
output_job = Job()
|
||||
input_job.add_output_dataset_collection("output_hdca", center_hdca)
|
||||
input_job.add_input_dataset("input_hda", input_hda1)
|
||||
input_job.add_input_dataset("input_hda", input_hda2)
|
||||
input_job.add_input_dataset_collection("input_hdca", input_hdca)
|
||||
output_job.add_input_dataset_collection("input_hdca", center_hdca)
|
||||
output_job.add_output_dataset("output_hda", output_hda)
|
||||
output_job.add_output_dataset_collection("output_hdca", output_hdca)
|
||||
sa_session.add_all([center_hdca, input_hda1, input_hda2, input_hdca, output_hdca, input_job, output_job])
|
||||
sa_session.flush()
|
||||
expected_graph = {
|
||||
"inputs": [
|
||||
{"src": "HistoryDatasetAssociation", "id": input_hda1.id},
|
||||
{"src": "HistoryDatasetAssociation", "id": input_hda2.id},
|
||||
{"src": "HistoryDatasetCollectionAssociation", "id": input_hdca.id},
|
||||
],
|
||||
"outputs": [
|
||||
{"src": "HistoryDatasetAssociation", "id": output_hda.id},
|
||||
{"src": "HistoryDatasetCollectionAssociation", "id": output_hdca.id},
|
||||
],
|
||||
}
|
||||
return center_hdca, expected_graph
|
||||
|
||||
|
||||
# =============================================================================
|
||||
def test_graph_manager_inputs_for_hda(job_connections_manager: JobConnectionsManager):
|
||||
sa_session = job_connections_manager.sa_session
|
||||
center_hda, expected_graph = setup_connected_dataset(sa_session)
|
||||
s = job_connections_manager.inputs_for_hda(center_hda.id)
|
||||
assert len(sa_session.execute(union(*s)).all()) == 2
|
||||
|
||||
|
||||
def test_graph_manager_outputs_for_hda(job_connections_manager: JobConnectionsManager):
|
||||
sa_session = job_connections_manager.sa_session
|
||||
center_hda, expected_graph = setup_connected_dataset(sa_session)
|
||||
s = job_connections_manager.outputs_derived_from_input_hda(center_hda.id)
|
||||
assert len(sa_session.execute(union(*s)).all()) == 2
|
||||
|
||||
|
||||
def test_graph_manager_inputs_for_hdca(job_connections_manager: JobConnectionsManager):
|
||||
sa_session = job_connections_manager.sa_session
|
||||
center_hdca, expected_graph = setup_connected_dataset_collection(sa_session)
|
||||
s = job_connections_manager.inputs_for_hdca(center_hdca.id)
|
||||
assert len(sa_session.execute(union(*s)).all()) == 3
|
||||
|
||||
|
||||
def test_graph_manager_outputs_for_hdca(job_connections_manager: JobConnectionsManager):
|
||||
sa_session = job_connections_manager.sa_session
|
||||
center_hdca, expected_graph = setup_connected_dataset_collection(sa_session)
|
||||
s = job_connections_manager.outputs_derived_from_input_hdca(center_hdca.id)
|
||||
assert len(sa_session.execute(union(*s)).all()) == 2
|
||||
|
||||
|
||||
def test_graph_manager_hda(job_connections_manager: JobConnectionsManager):
|
||||
center_hda, expected_graph = setup_connected_dataset(job_connections_manager.sa_session)
|
||||
assert job_connections_manager.get_connections_graph(center_hda.id, "HistoryDatasetAssociation") == expected_graph
|
||||
|
||||
|
||||
def test_graph_manager_hdca(job_connections_manager: JobConnectionsManager):
|
||||
center_hdca, expected_graph = setup_connected_dataset_collection(job_connections_manager.sa_session)
|
||||
assert (
|
||||
job_connections_manager.get_connections_graph(center_hdca.id, "HistoryDatasetCollectionAssociation")
|
||||
== expected_graph
|
||||
)
|
||||
Reference in New Issue
Block a user