diff --git a/client/src/components/History/HistoryView.vue b/client/src/components/History/HistoryView.vue
index 103909b0a9f7..e185e7517147 100644
--- a/client/src/components/History/HistoryView.vue
+++ b/client/src/components/History/HistoryView.vue
@@ -25,7 +25,9 @@
- History imported and set to your active history.
+
+ History imported and is now your active history. View here.
+
str:
+ return f"Problem listing file source path {file_source_path.file_source.get_uri_root()}{file_source_path.path}"
+
def get_files_source_plugins(
self,
user_context: ProvidesUserContext,
@@ -162,6 +166,9 @@ def create_entry(self, user_ctx: ProvidesUserContext, entry_data: CreateEntryPay
file_source = file_source_path.file_source
try:
result = file_source.create_entry(entry_data.dict(), user_context=user_file_source_context)
+ except exceptions.MessageException:
+ log.warning(f"Problem creating entry {entry_data.name} in file source {entry_data.target}", exc_info=True)
+ raise
except Exception:
message = f"Problem creating entry {entry_data.name} in file source {entry_data.target}"
log.warning(message, exc_info=True)
diff --git a/lib/galaxy/model/tags.py b/lib/galaxy/model/tags.py
index 5c77f76dc163..54cc8ccd72e8 100644
--- a/lib/galaxy/model/tags.py
+++ b/lib/galaxy/model/tags.py
@@ -296,7 +296,14 @@ def get_tag_by_name(self, tag_name):
return None
def _create_tag(self, tag_str: str):
- """Create a Tag object from a tag string."""
+ """
+ Create or retrieve one or more Tag objects from a tag string. If there are multiple
+ hierarchical tags in the tag string, the string will be split along `self.hierarchy_separator` chars.
+ A Tag instance will be created for each non-empty prefix. If a prefix corresponds to the
+ name of an existing tag, that tag will be retrieved; otherwise, a new Tag object will be created.
+ For example, for the tag string `a.b.c` 3 Tag instances will be created: `a`, `a.b`, `a.b.c`.
+ Return the last tag created (`a.b.c`).
+ """
tag_hierarchy = tag_str.split(self.hierarchy_separator)
tag_prefix = ""
parent_tag = None
diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py
index 34fe24988095..277bb82f481e 100644
--- a/lib/galaxy/schema/schema.py
+++ b/lib/galaxy/schema/schema.py
@@ -66,6 +66,8 @@
OptionalNumberT = Annotated[Optional[Union[int, float]], Field(None)]
+TAG_ITEM_PATTERN = r"^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$"
+
class DatasetState(str, Enum):
NEW = "new"
@@ -527,7 +529,7 @@ class HistoryContentSource(str, Enum):
DatasetCollectionInstanceType = Literal["history", "library"]
-TagItem = Annotated[str, Field(..., pattern=r"^([^\s.:])+(.[^\s.:]+)*(:[^\s.:]+)?$")]
+TagItem = Annotated[str, Field(..., pattern=TAG_ITEM_PATTERN)]
class TagCollection(RootModel):
diff --git a/lib/galaxy_test/api/test_item_tags.py b/lib/galaxy_test/api/test_item_tags.py
index 3b9527f7b37f..025c3504c012 100644
--- a/lib/galaxy_test/api/test_item_tags.py
+++ b/lib/galaxy_test/api/test_item_tags.py
@@ -135,9 +135,9 @@ def _create_valid_tag(self, prefix: str):
return response
def _create_history_contents(self, history_id):
- history_content_id = self.dataset_collection_populator.create_list_in_history(
- history_id, contents=["test_dataset"], direct_upload=True, wait=True
- ).json()["outputs"][0]["id"]
+ history_content_id = self.dataset_populator.new_dataset(
+ history_id, contents="test_dataset", direct_upload=True, wait=True
+ )["id"]
return history_content_id
def _create_history(self):
diff --git a/test/unit/app/managers/test_TagHandler.py b/test/unit/app/managers/test_TagHandler.py
index 97b476c0e715..7f0e3ad21ce8 100644
--- a/test/unit/app/managers/test_TagHandler.py
+++ b/test/unit/app/managers/test_TagHandler.py
@@ -112,3 +112,15 @@ def test_item_has_tag(self):
# Tag
assert self.tag_handler.item_has_tag(self.user, item=hda, tag=hda.tags[0].tag)
assert not self.tag_handler.item_has_tag(self.user, item=hda, tag="tag2")
+
+ def test_get_name_value_pair(self):
+ """Verify that parsing a single tag string correctly splits it into name/value pairs."""
+ assert self.tag_handler.parse_tags("a") == [("a", None)]
+ assert self.tag_handler.parse_tags("a.b") == [("a.b", None)]
+ assert self.tag_handler.parse_tags("a.b:c") == [("a.b", "c")]
+ assert self.tag_handler.parse_tags("a.b:c.d") == [("a.b", "c.d")]
+ assert self.tag_handler.parse_tags("a.b:c.d:e.f") == [("a.b", "c.d:e.f")]
+ assert self.tag_handler.parse_tags("a.b:c.d:e.f.") == [("a.b", "c.d:e.f.")]
+ assert self.tag_handler.parse_tags("a.b:c.d:e.f..") == [("a.b", "c.d:e.f..")]
+ assert self.tag_handler.parse_tags("a.b:c.d:e.f:") == [("a.b", "c.d:e.f:")]
+ assert self.tag_handler.parse_tags("a.b:c.d:e.f::") == [("a.b", "c.d:e.f::")]
diff --git a/test/unit/schema/test_schema.py b/test/unit/schema/test_schema.py
index 570469ed6fc2..21b37096d131 100644
--- a/test/unit/schema/test_schema.py
+++ b/test/unit/schema/test_schema.py
@@ -1,8 +1,12 @@
+import re
from uuid import uuid4
from pydantic import BaseModel
-from galaxy.schema.schema import DatasetStateField
+from galaxy.schema.schema import (
+ DatasetStateField,
+ TAG_ITEM_PATTERN,
+)
from galaxy.schema.tasks import (
GenerateInvocationDownload,
RequestUser,
@@ -34,3 +38,43 @@ class StateModel(BaseModel):
def test_dataset_state_coercion():
assert StateModel(state="ok").state == "ok"
assert StateModel(state="deleted").state == "discarded"
+
+
+class TestTagPattern:
+
+ def test_valid(self):
+ tag_strings = [
+ "a",
+ "aa",
+ "aa.aa",
+ "aa.aa.aa",
+ "~!@#$%^&*()_+`-=[]{};'\",./<>?",
+ "a.b:c",
+ "a.b:c.d:e.f",
+ "a.b:c.d:e..f",
+ "a.b:c.d:e.f:g",
+ "a.b:c.d:e.f::g",
+ "a.b:c.d:e.f::g:h",
+ "a::a", # leading colon for tag value
+ "a:.a", # leading period for tag value
+ "a:a:", # trailing colon OK for tag value
+ "a:a.", # trailing period OK for tag value
+ ]
+ for t in tag_strings:
+ assert re.match(TAG_ITEM_PATTERN, t)
+
+ def test_invalid(self):
+ tag_strings = [
+ " a", # leading space for tag name
+ ":a", # leading colon for tag name
+ ".a", # leading period for tag name
+ "a ", # trailing space for tag name
+ "a a", # space inside tag name
+ "a: a", # leading space for tag value
+ "a:a a", # space inside tag value
+ "a:", # trailing colon for tag name
+ "a.", # trailing period for tag name
+ "a:b ", # trailing space for tag value
+ ]
+ for t in tag_strings:
+ assert not re.match(TAG_ITEM_PATTERN, t)