Loading client/src/components/Tags/model.js +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { keyedColorScheme } from "utils/color"; // Valid tag regex. The basic format here is a tag name with optional subtags // separated by a period, and then an optional value after a colon. export const VALID_TAG_RE = /^([^\s.:])+(.[^\s.:]+)*(:[^\s.:]+)?$/; export const VALID_TAG_RE = /^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$/; export class TagModel { /** Loading lib/galaxy/model/tags.py +8 −1 Original line number Diff line number Diff line Loading @@ -295,7 +295,14 @@ class TagHandler: 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 Loading lib/galaxy/schema/schema.py +3 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ IMPLICIT_COLLECTION_JOBS_MODEL_CLASS = Literal["ImplicitCollectionJobs"] OptionalNumberT = Annotated[Optional[Union[int, float]], Field(None)] TAG_ITEM_PATTERN = r"^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$" class DatasetState(str, Enum): NEW = "new" Loading Loading @@ -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): Loading test/unit/app/managers/test_TagHandler.py +12 −0 Original line number Diff line number Diff line Loading @@ -112,3 +112,15 @@ class TestTagHandler(BaseTestCase): # 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::")] test/unit/schema/test_schema.py +45 −1 Original line number Diff line number Diff line 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, Loading Loading @@ -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) Loading
client/src/components/Tags/model.js +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { keyedColorScheme } from "utils/color"; // Valid tag regex. The basic format here is a tag name with optional subtags // separated by a period, and then an optional value after a colon. export const VALID_TAG_RE = /^([^\s.:])+(.[^\s.:]+)*(:[^\s.:]+)?$/; export const VALID_TAG_RE = /^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$/; export class TagModel { /** Loading
lib/galaxy/model/tags.py +8 −1 Original line number Diff line number Diff line Loading @@ -295,7 +295,14 @@ class TagHandler: 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 Loading
lib/galaxy/schema/schema.py +3 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ IMPLICIT_COLLECTION_JOBS_MODEL_CLASS = Literal["ImplicitCollectionJobs"] OptionalNumberT = Annotated[Optional[Union[int, float]], Field(None)] TAG_ITEM_PATTERN = r"^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$" class DatasetState(str, Enum): NEW = "new" Loading Loading @@ -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): Loading
test/unit/app/managers/test_TagHandler.py +12 −0 Original line number Diff line number Diff line Loading @@ -112,3 +112,15 @@ class TestTagHandler(BaseTestCase): # 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::")]
test/unit/schema/test_schema.py +45 −1 Original line number Diff line number Diff line 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, Loading Loading @@ -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)