diff --git a/lib/galaxy/managers/history_contents.py b/lib/galaxy/managers/history_contents.py index d5734aa2aa7..068020b63d5 100644 --- a/lib/galaxy/managers/history_contents.py +++ b/lib/galaxy/managers/history_contents.py @@ -531,34 +531,23 @@ class HistoryContentsFilters( 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") + has_related_q = [q for q in ("related-eq", "related") if query_filters.q and q in query_filters.q] + if query_filters.q and query_filters.qv and has_related_q: + qv_index = query_filters.q.index(has_related_q[0]) qv_hid = query_filters.qv[qv_index] # Type check whether hid is int if not qv_hid.isdigit(): raise glx_exceptions.RequestParameterInvalidException( - "unparsable value for filter", + "unparsable value for related filter", column="related", operation="eq", value=qv_hid, ValueError="invalid type in filter", ) - # 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, + query_filters_with_relations = self.get_query_filters_with_relations( + query_filters=query_filters, related_q=has_related_q[0], history_id=history_id ) return super().parse_query_filters(query_filters_with_relations) return super().parse_query_filters(query_filters) @@ -617,6 +606,30 @@ class HistoryContentsFilters( return self.parsed_filter(filter_type="orm", filter=column_filter) return super()._parse_orm_filter(attr, op, val) + def get_query_filters_with_relations(self, query_filters: ValueFilterQueryParams, related_q: str, history_id): + """Return `query_filters_with_relations` changing `related:hid` to `related:[hid1, hid2, ...]`.""" + if query_filters.q and query_filters.qv: + qv_index = query_filters.q.index(related_q) + 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, int(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 query_filters_with_relations + return query_filters + def decode_type_id(self, type_id): TYPE_ID_SEP = "-" split = type_id.split(TYPE_ID_SEP, 1) @@ -638,8 +651,6 @@ class HistoryContentsFilters( self.orm_filter_parsers.update( { "history_content_type": {"op": ("eq")}, - # maybe remove related from here, as there's no corresponding field? - "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 index df3363ceee5..e387f36527a 100644 --- a/lib/galaxy/managers/job_connections.py +++ b/lib/galaxy/managers/job_connections.py @@ -18,7 +18,7 @@ class JobConnectionsManager: def __init__(self, sa_session: galaxy_scoped_session): self.sa_session = sa_session - def get_connections_graph(self, id, src): + def get_connections_graph(self, id: int, src: str): """Get connections graph of inputs and outputs for given item id""" if src == "HistoryDatasetAssociation": output_selects = self.outputs_derived_from_input_hda(id) @@ -34,7 +34,7 @@ class JobConnectionsManager: result["inputs"] = self._get_union_results(*input_selects) return result - def get_related_hids(self, history_id, hid): + def get_related_hids(self, history_id, hid: int): """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( @@ -63,7 +63,7 @@ class JobConnectionsManager: result.append({"src": row.src, "id": row.id}) return result - def outputs_derived_from_input_hda(self, input_hda_id): + def outputs_derived_from_input_hda(self, input_hda_id: int): hda_select = ( select( [ @@ -94,7 +94,7 @@ class JobConnectionsManager: ) return hda_select, hdca_select - def outputs_derived_from_input_hdca(self, input_hdca_id): + def outputs_derived_from_input_hdca(self, input_hdca_id: int): hda_select = ( select( [ @@ -126,7 +126,7 @@ class JobConnectionsManager: ) return hda_select, hdca_select - def inputs_for_hda(self, input_hda_id): + def inputs_for_hda(self, input_hda_id: int): input_hdas = ( select( [ @@ -157,7 +157,7 @@ class JobConnectionsManager: ) return input_hdas, input_hdcas - def inputs_for_hdca(self, input_hdca_id): + def inputs_for_hdca(self, input_hdca_id: int): input_hdas = ( select( [ diff --git a/lib/galaxy_test/api/test_history_contents.py b/lib/galaxy_test/api/test_history_contents.py index 427f6178053..f92bdd4577f 100644 --- a/lib/galaxy_test/api/test_history_contents.py +++ b/lib/galaxy_test/api/test_history_contents.py @@ -737,6 +737,38 @@ class TestHistoryContentsApi(ApiTestCase): ).json() assert len(contents_response) == 0 + @skip_without_tool("cat_data_and_sleep") + def test_index_filter_by_related_items(self, history_id): + # initialise history with 2 datasets + input_hda_id = self.dataset_populator.new_dataset(history_id)["id"] + unrelated_hid = self.dataset_populator.new_dataset(history_id)["hid"] + + # Run tool on first dataset to get 3rd, related dataset + inputs = { + "input1": {"src": "hda", "id": input_hda_id}, + "sleep_time": 0, + } + run_response = self.dataset_populator.run_tool_raw( + "cat_data_and_sleep", + inputs, + history_id, + ) + related_hid = run_response.json()["outputs"][0]["hid"] + + # Test q = related-eq, for related items + contents_response = self._get(f"histories/{history_id}/contents?v=dev&q=related-eq&qv={related_hid}").json() + assert len(contents_response) == 2 + + # Test q = related, for unrelated item + contents_response = self._get(f"histories/{history_id}/contents?v=dev&q=related&qv={unrelated_hid}").json() + assert len(contents_response) == 1 + + # Test error case: qv is string + related_qv = "one" + contents_response = self._get(f"histories/{history_id}/contents?v=dev&q=related-eq&qv={related_qv}") + assert contents_response.status_code == 400 + assert contents_response.json()["err_msg"] == "unparsable value for related filter" + def test_elements_datatypes_field(self, history_id): collection_name = "homogeneous" expected_datatypes = ["txt"]