diff --git a/client/src/components/History/Content/ContentItem.vue b/client/src/components/History/Content/ContentItem.vue index b57282a2e36..34171c74de5 100644 --- a/client/src/components/History/Content/ContentItem.vue +++ b/client/src/components/History/Content/ContentItem.vue @@ -14,27 +14,34 @@ - + - - + - - - - + + + + @@ -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, diff --git a/client/src/components/History/Content/model/highlights.js b/client/src/components/History/Content/model/highlights.js deleted file mode 100644 index 7ae21b5905a..00000000000 --- a/client/src/components/History/Content/model/highlights.js +++ /dev/null @@ -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; -} diff --git a/client/src/components/History/CurrentHistory/HistoryFilters/HistoryFilters.vue b/client/src/components/History/CurrentHistory/HistoryFilters/HistoryFilters.vue index 5d4a015db72..12a81ffe24b 100644 --- a/client/src/components/History/CurrentHistory/HistoryFilters/HistoryFilters.vue +++ b/client/src/components/History/CurrentHistory/HistoryFilters/HistoryFilters.vue @@ -46,6 +46,8 @@ Filter by state: + Filter by related to item index: + Filter by item index: diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue index 187c6f945d1..cb453905505 100644 --- a/client/src/components/History/CurrentHistory/HistoryPanel.vue +++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue @@ -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; diff --git a/client/src/components/History/HistoryFilters.js b/client/src/components/History/HistoryFilters.js index f61d53f5883..e8d9dd9bb8e 100644 --- a/client/src/components/History/HistoryFilters.js +++ b/client/src/components/History/HistoryFilters.js @@ -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), diff --git a/client/src/stores/history/historyItemsStore.js b/client/src/stores/history/historyItemsStore.js index ea2f282193d..7a0563dc297 100644 --- a/client/src/stores/history/historyItemsStore.js +++ b/client/src/stores/history/historyItemsStore.js @@ -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); + } }); }); }, diff --git a/client/src/utils/filtering.test.js b/client/src/utils/filtering.test.js index b7f877a9ab0..da88b73a243 100644 --- a/client/src/utils/filtering.test.js +++ b/client/src/utils/filtering.test.js @@ -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); diff --git a/client/src/utils/filtering.ts b/client/src/utils/filtering.ts index 616bd3a3858..5ea02047fb8 100644 --- a/client/src/utils/filtering.ts +++ b/client/src/utils/filtering.ts @@ -18,6 +18,14 @@ const defaultValidAliases = [ ["<", "_lt"], ]; +const operatorForAlias: Record = { + 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 = { attribute: string; converter?: Converter; @@ -323,12 +339,28 @@ export default class Filtering { * @returns {Boolean} True if the filter is set to the given value * */ checkFilter(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 diff --git a/lib/galaxy/managers/history_contents.py b/lib/galaxy/managers/history_contents.py index 34131c07af7..055a8b94687 100644 --- a/lib/galaxy/managers/history_contents.py +++ b/lib/galaxy/managers/history_contents.py @@ -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 diff --git a/lib/galaxy/managers/job_connections.py b/lib/galaxy/managers/job_connections.py new file mode 100644 index 00000000000..c52480c93d2 --- /dev/null +++ b/lib/galaxy/managers/job_connections.py @@ -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 diff --git a/lib/galaxy/webapps/galaxy/services/history_contents.py b/lib/galaxy/webapps/galaxy/services/history_contents.py index dbbf09b269e..0c72c4b497c 100644 --- a/lib/galaxy/webapps/galaxy/services/history_contents.py +++ b/lib/galaxy/webapps/galaxy/services/history_contents.py @@ -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): diff --git a/test/unit/app/managers/test_JobConnectionsManager.py b/test/unit/app/managers/test_JobConnectionsManager.py new file mode 100644 index 00000000000..844b4dbdcdf --- /dev/null +++ b/test/unit/app/managers/test_JobConnectionsManager.py @@ -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 + )