diff --git a/specifyweb/backend/stored_queries/queryfieldspec.py b/specifyweb/backend/stored_queries/queryfieldspec.py index ceec7f6c8bf..dd11312a559 100644 --- a/specifyweb/backend/stored_queries/queryfieldspec.py +++ b/specifyweb/backend/stored_queries/queryfieldspec.py @@ -235,12 +235,6 @@ def from_stringid(cls, stringid: str, is_relation: bool): path = deque(path_str.split(",")) root_table = datamodel.get_table_by_id(int(path.popleft())) - if is_relation: - extracted_fieldname, _ = extract_date_part(field_name) - root_field = root_table.get_field(extracted_fieldname, strict=False) - if isinstance(root_field, Relationship): - path.pop() - join_path = [] node = root_table for elem in path: @@ -256,6 +250,44 @@ def from_stringid(cls, stringid: str, is_relation: bool): node = table extracted_fieldname, date_part = extract_date_part(field_name) + + if is_relation: + relation = None + if join_path and isinstance(join_path[-1], Relationship): + last_relation = join_path[-1] + related_table = datamodel.get_table( + last_relation.relatedModelName, strict=True + ) + if related_table.name.lower() == table_name.lower(): + relation = last_relation + + if relation is None: + relation = node.get_field(extracted_fieldname, strict=False) + if isinstance(relation, Relationship): + join_path.append(relation) + node = datamodel.get_table(relation.relatedModelName, strict=True) + else: + relation = None + + if relation is not None: + result = cls( + root_table=root_table, + root_sql_table=getattr(models, root_table.name), + join_path=tuple(join_path), + table=node, + date_part=None, + tree_rank=None, + tree_field=relation, + ) + logger.debug( + "parsed %s (is_relation %s) to %s. extracted_fieldname = %s", + stringid, + is_relation, + result, + extracted_fieldname, + ) + return result + field = node.get_field(extracted_fieldname, strict=False) tree_rank_name = None diff --git a/specifyweb/backend/stored_queries/tests/static/co_query_row_plan.py b/specifyweb/backend/stored_queries/tests/static/co_query_row_plan.py index 09008570f2f..d00943eb4a6 100644 --- a/specifyweb/backend/stored_queries/tests/static/co_query_row_plan.py +++ b/specifyweb/backend/stored_queries/tests/static/co_query_row_plan.py @@ -41,11 +41,11 @@ ), "collectingevent": RowPlanMap( batch_edit_pack=BatchEditPack( - id=BatchEditFieldPack(field=None, idx=18, value=None), + id=BatchEditFieldPack(field=None, idx=19, value=None), order=BatchEditFieldPack(field=None, idx=None, value=None), - version=BatchEditFieldPack(field=None, idx=19, value=None), + version=BatchEditFieldPack(field=None, idx=20, value=None), ), - columns=[], + columns=[BatchEditFieldPack(field=None, idx=18, value=None)], to_one={ "locality": RowPlanMap( batch_edit_pack=BatchEditPack( @@ -54,7 +54,6 @@ version=BatchEditFieldPack(field=None, idx=31, value=None), ), columns=[ - BatchEditFieldPack(field=None, idx=20, value=None), BatchEditFieldPack(field=None, idx=21, value=None), BatchEditFieldPack(field=None, idx=22, value=None), BatchEditFieldPack(field=None, idx=23, value=None), diff --git a/specifyweb/backend/stored_queries/tests/static/simple_static_fields.py b/specifyweb/backend/stored_queries/tests/static/simple_static_fields.py index a84b3664c07..4afbd44c120 100644 --- a/specifyweb/backend/stored_queries/tests/static/simple_static_fields.py +++ b/specifyweb/backend/stored_queries/tests/static/simple_static_fields.py @@ -170,12 +170,12 @@ def get_sql_table(name): "cataloger" ), ), - "table": datamodel.get_table_strict("CollectionObject"), + "table": datamodel.get_table_strict("Agent"), "date_part": None, "tree_rank": None, - "tree_field": datamodel.get_table_strict( - "CollectionObject" - ).get_field_strict("cataloger"), + "tree_field": datamodel.get_table_strict("CollectionObject").get_field_strict( + "cataloger" + ), } ), op_num=8, @@ -308,12 +308,12 @@ def get_sql_table(name): "determinations" ), ), - "table": datamodel.get_table_strict("CollectionObject"), + "table": datamodel.get_table_strict("Determination"), "date_part": None, "tree_rank": None, - "tree_field": datamodel.get_table_strict( - "CollectionObject" - ).get_field_strict("determinations"), + "tree_field": datamodel.get_table_strict("CollectionObject").get_field_strict( + "determinations" + ), } ), op_num=8, @@ -334,12 +334,12 @@ def get_sql_table(name): "preparations" ), ), - "table": datamodel.get_table_strict("CollectionObject"), + "table": datamodel.get_table_strict("Preparation"), "date_part": None, "tree_rank": None, - "tree_field": datamodel.get_table_strict( - "CollectionObject" - ).get_field_strict("preparations"), + "tree_field": datamodel.get_table_strict("CollectionObject").get_field_strict( + "preparations" + ), } ), op_num=8, @@ -387,12 +387,12 @@ def get_sql_table(name): "collectingEvent" ), ), - "table": datamodel.get_table_strict("CollectionObject"), + "table": datamodel.get_table_strict("CollectingEvent"), "date_part": None, "tree_rank": None, - "tree_field": datamodel.get_table_strict( - "CollectionObject" - ).get_field_strict("collectingEvent"), + "tree_field": datamodel.get_table_strict("CollectionObject").get_field_strict( + "collectingEvent" + ), } ), op_num=8, @@ -415,19 +415,13 @@ def get_sql_table(name): datamodel.get_table_strict("CollectingEvent").get_field_strict( "locality" ), - TreeRankQuery( - **{ - "name": "locality", - "relatedModelName": "Locality", - "type": "many-to-one", - "column": "localityId", - } - ), ), "table": datamodel.get_table_strict("Locality"), "date_part": None, - "tree_rank": "locality", - "tree_field": None, + "tree_rank": None, + "tree_field": datamodel.get_table_strict("CollectingEvent").get_field_strict( + "locality" + ), } ), op_num=8, diff --git a/specifyweb/backend/stored_queries/tests/test_execution/test_field_specs_from_json.py b/specifyweb/backend/stored_queries/tests/test_execution/test_field_specs_from_json.py index c317fa53cf1..6749e225c89 100644 --- a/specifyweb/backend/stored_queries/tests/test_execution/test_field_specs_from_json.py +++ b/specifyweb/backend/stored_queries/tests/test_execution/test_field_specs_from_json.py @@ -101,6 +101,20 @@ def generate_fields_test_str(query_fields, var_name): # pragma: no cover class TestFieldSpecsFromJson(ApiTests): + def assert_relation_stringid(self, stringid: str, expected_path: tuple[str, ...]): + fieldspec = QueryFieldSpec.from_stringid(stringid, True) + round_tripped = QueryFieldSpec.from_stringid(fieldspec.to_stringid(), True) + + self.assertTrue(fieldspec.is_relationship()) + self.assertEqual(fieldspec, round_tripped) + self.assertEqual( + tuple(node.name for node in fieldspec.join_path), + expected_path, + ) + self.assertFalse( + any(isinstance(node, TreeRankQuery) for node in fieldspec.join_path) + ) + def test_static_field_specs(self): # pragma: no cover query = json.load(open("specifyweb/backend/stored_queries/tests/static/co_query.json")) query_fields = fields_from_json(query["fields"]) @@ -108,3 +122,27 @@ def test_static_field_specs(self): # pragma: no cover # generate_fields_test_str(query_fields, "static_simple_field_spec") self.assertEqual(static_simple_field_spec, query_fields) + + def test_nested_formatted_prep_type_stringid_stays_relationship(self): + self.assert_relation_stringid( + "1,63-preparations,65.preptype.prepType", + ("preparations", "prepType"), + ) + + def test_direct_formatted_collecting_event_stringid_stays_relationship(self): + self.assert_relation_stringid( + "1,10.collectingevent.collectingEvent", + ("collectingEvent",), + ) + + def test_nested_formatted_taxon_stringid_stays_relationship(self): + self.assert_relation_stringid( + "1,9-determinations,4.taxon.taxon", + ("determinations", "taxon"), + ) + + def test_nested_formatted_chronostrat_stringid_stays_relationship(self): + self.assert_relation_stringid( + "1,32,46-chronosStrat.geologictimeperiod.chronosStrat", + ("paleoContext", "chronosStrat"), + ) diff --git a/specifyweb/backend/stored_queries/tests/tests_legacy.py b/specifyweb/backend/stored_queries/tests/tests_legacy.py index cb34b27ae22..af9952a5350 100644 --- a/specifyweb/backend/stored_queries/tests/tests_legacy.py +++ b/specifyweb/backend/stored_queries/tests/tests_legacy.py @@ -1,4 +1,4 @@ -from unittest import TestCase, expectedFailure, skip +from unittest import TestCase, skip from sqlalchemy import orm, inspect import specifyweb.specify.models as spmodels @@ -11,7 +11,6 @@ def test_stringid_roundtrip_from_bug(self) -> None: fs = QueryFieldSpec.from_stringid("4.taxon.Genus", False) self.assertEqual("4.taxon.Genus", fs.to_stringid()) - @expectedFailure def test_stringid_roundtrip_en_masse(self) -> None: for (stringid, relfld) in STRINGID_LIST: fs = QueryFieldSpec.from_stringid(stringid, relfld)