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)