From 3b3d8135d9aa489f48025365c0445fdaea846345 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 28 Mar 2026 10:26:03 +0100 Subject: [PATCH 1/5] Fix nested DatasetCollectionElement in dynamic options When a DatasetCollectionElement containing an LDDA or nested child collection (rather than an HDA) was used as a reference for dynamic option filtering, _get_ref_data() failed to normalize it into a list, causing "TypeError: 'DatasetCollectionElement' object is not iterable". Use DatasetCollectionElement.dataset_instances which correctly returns a list of dataset instances for all element types (HDA, LDDA, or nested collections). Fixes galaxyproject#22099 --- lib/galaxy/tools/parameters/dynamic_options.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/tools/parameters/dynamic_options.py b/lib/galaxy/tools/parameters/dynamic_options.py index a736a0d385b..3474df91b9b 100644 --- a/lib/galaxy/tools/parameters/dynamic_options.py +++ b/lib/galaxy/tools/parameters/dynamic_options.py @@ -1079,10 +1079,10 @@ def _get_ref_data(other_values, ref_name): if is_runtime_value(ref): return [] raise ValueError - if isinstance(ref, DatasetCollectionElement) and ref.hda: - ref = ref.hda + if isinstance(ref, DatasetCollectionElement): + return ref.dataset_instances if isinstance(ref, (DatasetFilenameWrapper, HistoryDatasetAssociation, LibraryDatasetDatasetAssociation)): - ref = [ref] + return [ref] elif isinstance(ref, HistoryDatasetCollectionAssociation): - ref = ref.to_hda_representative(multiple=True) + return ref.to_hda_representative(multiple=True) return ref From 891a82249152040ba38b982c56e6ae140f125746 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 28 Mar 2026 10:31:40 +0100 Subject: [PATCH 2/5] Fix AttributeError when invalid tool ID passed to shed_tool_static Return a 404 instead of crashing with an unhandled AttributeError when toolbox.get_tool() returns None for an invalid/unknown tool GUID. Fixes https://github.com/galaxyproject/galaxy/issues/22126 --- lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py b/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py index 069ff8a8f01..6e88939b2ee 100644 --- a/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py +++ b/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py @@ -2,7 +2,10 @@ import logging import os from galaxy import web -from galaxy.exceptions import RequestParameterInvalidException +from galaxy.exceptions import ( + ObjectNotFound, + RequestParameterInvalidException, +) from galaxy.util.path import ( join, safe_contains, @@ -34,6 +37,8 @@ class ShedToolStatic(BaseUIController): """ guid = "/".join((shed, "repos", owner, repo, tool, version)) tool = trans.app.toolbox.get_tool(guid) + if tool is None: + raise ObjectNotFound(f"Could not find tool with guid '{guid}'.") repo_path = os.path.abspath(tool._repository_dir) found_path = None From 4a686851a1c64ca78c5d38df5159accc2d283475 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 28 Mar 2026 10:31:40 +0100 Subject: [PATCH 3/5] URL-encode tool ID in shed_tool_static image paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool IDs with special characters (e.g., "EMBOSS: isochore47") were embedded into URL paths without percent-encoding. The space would be lost in transit through RST→HTML→browser, causing the server to reconstruct a GUID that didn't match any tool. Apply urllib.parse.quote(safe="/") to the entire route_to_images path so special characters are properly percent-encoded. The WSGI layer auto-decodes PATH_INFO before routing, so the controller receives the correct values. Fixes https://github.com/galaxyproject/galaxy/issues/22126 --- lib/galaxy/tool_shed/util/shed_util_common.py | 6 +- test/unit/tool_shed/test_shed_util_common.py | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/unit/tool_shed/test_shed_util_common.py diff --git a/lib/galaxy/tool_shed/util/shed_util_common.py b/lib/galaxy/tool_shed/util/shed_util_common.py index 648b9de4eaf..90b8079f8a0 100644 --- a/lib/galaxy/tool_shed/util/shed_util_common.py +++ b/lib/galaxy/tool_shed/util/shed_util_common.py @@ -1,5 +1,6 @@ import logging import re +from urllib.parse import quote from galaxy import util from galaxy.tool_shed.util import repository_util @@ -149,7 +150,10 @@ def set_image_paths(app, text, encoded_repository_id=None, tool_shed_repository= # We're in the tool shed. route_to_images = f"/repository/static/images/{encoded_repository_id}" elif tool_shed_repository and tool_id and tool_version: - route_to_images = f"shed_tool_static/{tool_shed_repository.tool_shed}/{tool_shed_repository.owner}/{tool_shed_repository.name}/{tool_id}/{tool_version}" + route_to_images = quote( + f"shed_tool_static/{tool_shed_repository.tool_shed}/{tool_shed_repository.owner}/{tool_shed_repository.name}/{tool_id}/{tool_version}", + safe="/", + ) else: raise Exception( "encoded_repository_id or tool_shed_repository and tool_id and tool_version must be provided" diff --git a/test/unit/tool_shed/test_shed_util_common.py b/test/unit/tool_shed/test_shed_util_common.py new file mode 100644 index 00000000000..3b49dfbec9b --- /dev/null +++ b/test/unit/tool_shed/test_shed_util_common.py @@ -0,0 +1,56 @@ +from types import SimpleNamespace + +from galaxy.tool_shed.util.shed_util_common import set_image_paths + + +def test_set_image_paths_encodes_special_characters_in_tool_id(): + tool_shed_repository = SimpleNamespace( + tool_shed="toolshed.g2.bx.psu.edu", + owner="devteam", + name="emboss_5", + ) + text = ".. image:: static/images/isochore.png" + result = set_image_paths( + app=None, + text=text, + tool_shed_repository=tool_shed_repository, + tool_id="EMBOSS: isochore47", + tool_version="5.0.0.1", + ) + assert "EMBOSS%3A%20isochore47" in result + assert "EMBOSS: isochore47" not in result + + +def test_set_image_paths_preserves_slashes_in_route(): + tool_shed_repository = SimpleNamespace( + tool_shed="toolshed.g2.bx.psu.edu", + owner="devteam", + name="emboss_5", + ) + text = ".. image:: isochore.png" + result = set_image_paths( + app=None, + text=text, + tool_shed_repository=tool_shed_repository, + tool_id="isochore", + tool_version="5.0.0", + ) + assert "shed_tool_static/toolshed.g2.bx.psu.edu/devteam/emboss_5/isochore/5.0.0/" in result + + +def test_set_image_paths_does_not_modify_http_urls(): + tool_shed_repository = SimpleNamespace( + tool_shed="toolshed.g2.bx.psu.edu", + owner="devteam", + name="emboss_5", + ) + text = ".. image:: https://example.com/image.png" + result = set_image_paths( + app=None, + text=text, + tool_shed_repository=tool_shed_repository, + tool_id="mytool", + tool_version="1.0", + ) + assert ".. image:: https://example.com/image.png" in result + assert "shed_tool_static" not in result From 07440fa58af10f8e7d00ecac0861a7a65d87fef5 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 28 Mar 2026 10:54:28 +0100 Subject: [PATCH 4/5] Add regression test for passing list DCE into dynamic options --- lib/galaxy_test/api/test_tools.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py index e8c35779792..d6cc99fce75 100644 --- a/lib/galaxy_test/api/test_tools.py +++ b/lib/galaxy_test/api/test_tools.py @@ -286,6 +286,26 @@ class TestToolsApi(ApiTestCase, TestsTools): assert "hg18_value" in option_values assert "mm10_value" in option_values + @skip_without_tool("dbkey_filter_collection_input") + def test_run_dbkey_filter_nested_collection_dce(self): + with self.dataset_populator.test_history() as history_id: + list_list = self.dataset_collection_populator.create_list_of_list_in_history(history_id, wait=True).json() + # Set dbkey on the datasets in the inner list + for outer_element in list_list["elements"]: + for inner_element in outer_element["object"]["elements"]: + hda_id = inner_element["object"]["id"] + self.dataset_populator._put( + f"histories/{history_id}/contents/{hda_id}", {"genome_build": "hg19"}, json=True + ) + # Get DCE ID of the inner list element - this is a DatasetCollectionElement + # wrapping a child collection (not an HDA) + dce_id = list_list["elements"][0]["id"] + inputs = { + "inputs": {"src": "dce", "id": dce_id}, + "index": "hg19_value", + } + self._run("dbkey_filter_collection_input", history_id, inputs, assert_ok=True) + @skip_without_tool("cheetah_problem_unbound_var_input") def test_legacy_biotools_xref_injection(self): url = self._api_url("tools/cheetah_problem_unbound_var_input") From 4c7988f2ae8e741db28059af4cf0a9bc7903361e Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sun, 29 Mar 2026 13:17:12 +0200 Subject: [PATCH 5/5] Fix HDCA shown as unavailable on tool form rerun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After 6a9323a79 (Fix HDCA lost in tool form on rerun), visible HDCAs in the current history were incorrectly added to the job_input_values fallback section with a "(not in current history)" label. This happened because active_visible_datasets_and_roles only iterates HDAs, so HDCAs were never removed from job_input_values during matching. The HDCA then appeared twice in the form: once correctly from the dataset collections section, and once as a "keep" option with wrong state. Skip HDCAs in the fallback loop when they are active, visible, and in the current history — they are already properly handled by the active_visible_dataset_collections loop below. Fixes `lib/galaxy_test/selenium/test_tool_form.py::TestToolForm::test_rerun_dataset_collection_element - selenium.common.exceptions.TimeoutException: Message: Timeout waiting on CSS selector [.dataset-collection-panel] to become present.` --- lib/galaxy/tools/parameters/basic.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index d25d46d21f8..a60053b711d 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -2452,15 +2452,25 @@ class DataToolParameter(BaseDataToolParameter): # Route each to the correct options list by type so the client can # match them by id *and* src. for value in job_input_values: - if isinstance(value, (HistoryDatasetCollectionAssociation, HistoryDatasetAssociation)): + if isinstance(value, HistoryDatasetCollectionAssociation): + # HDCAs are handled by the dataset collections section below; + # only add here if not visible in the current history. + if value.deleted or not value.visible or value.history != history: + if value.deleted: + state = "deleted" + elif not value.visible: + state = "hidden" + else: + state = "not in current history" + append(d["options"]["hdca"], value, f"({state}) {value.name}", "hdca", True) + elif isinstance(value, HistoryDatasetAssociation): if value.deleted: state = "deleted" elif not value.visible: state = "hidden" else: state = "not in current history" - src = "hdca" if isinstance(value, HistoryDatasetCollectionAssociation) else "hda" - append(d["options"][src], value, f"({state}) {value.name}", src, True) + append(d["options"]["hda"], value, f"({state}) {value.name}", "hda", True) elif isinstance(value, DatasetCollectionElement): append_dce(value) elif isinstance(value, LibraryDatasetDatasetAssociation):