From ecbdbf94078c1ece4dc2adbf445d3733df247a0a Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 16 Jan 2026 16:22:21 -0600 Subject: [PATCH 1/2] main unit test fixes --- .../tests/test_localityupdate_status.py | 10 ++--- .../tests/test_parse_field.py | 2 +- .../locality_update_tool/update_locality.py | 16 +++++-- specifyweb/backend/trees/extras.py | 42 ++++++++++++++++--- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/specifyweb/backend/locality_update_tool/tests/test_localityupdate_status.py b/specifyweb/backend/locality_update_tool/tests/test_localityupdate_status.py index acad98e50a6..5b6fceb161f 100644 --- a/specifyweb/backend/locality_update_tool/tests/test_localityupdate_status.py +++ b/specifyweb/backend/locality_update_tool/tests/test_localityupdate_status.py @@ -36,7 +36,7 @@ def test_localityupdate_not_exist(self): self._assertStatusCodeEqual(response, http.HttpResponseNotFound.status_code) self.assertEqual(response.content.decode(), f"The localityupdate with task id '{task_id}' was not found") - @patch("specifyweb.specify.views.update_locality_task.AsyncResult") + @patch("specifyweb.backend.locality_update_tool.views.update_locality_task.AsyncResult") def test_failed(self, AsyncResult: Mock): mock_result = Mock() mock_result.state = CELERY_TASK_STATE.FAILURE @@ -70,7 +70,7 @@ def test_failed(self, AsyncResult: Mock): } ) - @patch("specifyweb.specify.views.update_locality_task.AsyncResult") + @patch("specifyweb.backend.locality_update_tool.views.update_locality_task.AsyncResult") def test_parse_failed(self, AsyncResult: Mock): mock_result = Mock() mock_result.state = CELERY_TASK_STATE.SUCCESS @@ -98,7 +98,7 @@ def test_parse_failed(self, AsyncResult: Mock): } ) - @patch("specifyweb.specify.views.update_locality_task.AsyncResult") + @patch("specifyweb.backend.locality_update_tool.views.update_locality_task.AsyncResult") def test_parsed(self, AsyncResult: Mock): mock_result = Mock() mock_result.state = CELERY_TASK_STATE.SUCCESS @@ -149,7 +149,7 @@ def test_parsed(self, AsyncResult: Mock): } ) - @patch("specifyweb.specify.views.update_locality_task.AsyncResult") + @patch("specifyweb.backend.locality_update_tool.views.update_locality_task.AsyncResult") def test_succeeded(self, AsyncResult: Mock): mock_result = Mock() mock_result.state = LocalityUpdateStatus.SUCCEEDED @@ -181,7 +181,7 @@ def test_succeeded(self, AsyncResult: Mock): } ) - @patch("specifyweb.specify.views.update_locality_task.AsyncResult") + @patch("specifyweb.backend.locality_update_tool.views.update_locality_task.AsyncResult") def test_succeeded_locality_rows(self, AsyncResult: Mock): mock_result = Mock() mock_result.state = LocalityUpdateStatus.SUCCEEDED diff --git a/specifyweb/backend/locality_update_tool/tests/test_parse_field.py b/specifyweb/backend/locality_update_tool/tests/test_parse_field.py index 02e7f447433..3e512bb41df 100644 --- a/specifyweb/backend/locality_update_tool/tests/test_parse_field.py +++ b/specifyweb/backend/locality_update_tool/tests/test_parse_field.py @@ -33,7 +33,7 @@ def test_no_ui_formatter(self): self.assertEqual(parsed_with_value, parsed_with_value_result) - @patch("specifyweb.specify.update_locality.get_uiformatter") + @patch("specifyweb.backend.locality_update_tool.update_locality.get_uiformatter") def test_cnn_formatter(self, get_uiformatter: Mock): get_uiformatter.return_value = UIFormatter( diff --git a/specifyweb/backend/locality_update_tool/update_locality.py b/specifyweb/backend/locality_update_tool/update_locality.py index ae1873f1631..1fe2985daf8 100644 --- a/specifyweb/backend/locality_update_tool/update_locality.py +++ b/specifyweb/backend/locality_update_tool/update_locality.py @@ -379,11 +379,19 @@ def parse_locality_set(collection, raw_headers: list[str], data: list[list[str]] locality_id: int | None = None if len( locality_query) != 1 else locality_query[0].id - parsed_locality_fields = [parse_field( - collection, 'Locality', dict['field'], dict['value'], locality_id, row_number) for dict in locality_values if dict['value'].strip() != ""] + parsed_locality_fields = [ + parse_field( + collection, 'Locality', d['field'], d['value'], locality_id, row_number + ) + for d in locality_values + ] - parsed_geocoorddetail_fields = [parse_field( - collection, 'Geocoorddetail', dict["field"], dict['value'], locality_id, row_number) for dict in geocoorddetail_values if dict['value'].strip() != ""] + parsed_geocoorddetail_fields = [ + parse_field( + collection, 'Geocoorddetail', d['field'], d['value'], locality_id, row_number + ) + for d in geocoorddetail_values + ] parsed_row, parsed_errors = merge_parse_results( [*parsed_locality_fields, *parsed_geocoorddetail_fields], locality_id, row_number) diff --git a/specifyweb/backend/trees/extras.py b/specifyweb/backend/trees/extras.py index 52d16aec7d2..ace1cdf1e3e 100644 --- a/specifyweb/backend/trees/extras.py +++ b/specifyweb/backend/trees/extras.py @@ -411,18 +411,48 @@ def synonymize(node, into, agent, user=None, collection=None): # This check can be disabled by a remote pref import specifyweb.backend.context.app_resource as app_resource - collection_prefs_json, _, __ = app_resource.get_app_resource(collection, user, 'CollectionPreferences') - if collection_prefs_json is not None: - collection_prefs_dict = json.loads(collection_prefs_json) - treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) + collection_prefs_dict = {} # always defined + res = app_resource.get_app_resource(collection, user, 'CollectionPreferences') + force_checks = (collection is None or user is None) + if res is not None: + collection_prefs_json, _, __ = res + if collection_prefs_json: + try: + collection_prefs_dict = json.loads(collection_prefs_json) or {} + except Exception: + collection_prefs_dict = {} + + import specifyweb.backend.context.app_resource as app_resource + + treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) + if force_checks and target.children.exists(): + raise TreeBusinessRuleException( + f'Synonymizing "{node.fullname}" to "{into.fullname}" which has children', + {"tree": "Taxon", + "localizationKey": "nodeSynonimizeWithChildren", + "node": { + "id": node.id, + "rankid": node.rankid, + "fullName": node.fullname, + "children": list(node.children.values('id', 'fullname')) + }, + "parent": { + "id": into.id, + "rankid": into.rankid, + "fullName": into.fullname, + "parentid": into.parent.id, + "children": list(into.children.values('id', 'fullname')) + }} + ) + force_checks = (collection is None or user is None) synonymized = treeManagement_pref.get('synonymized', {}) \ if isinstance(treeManagement_pref, dict) else {} add_synonym_enabled = synonymized.get(r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)', False) if isinstance(synonymized, dict) else False - if node.children.count() > 0 and (add_synonym_enabled is True): + if node.children.count() > 0 and (force_checks or add_synonym_enabled is False): raise TreeBusinessRuleException( f'Synonymizing node "{node.fullname}" which has children', {"tree" : "Taxon", @@ -840,4 +870,4 @@ def tree_path_expr(tbl: str, d: int) -> str: # replace path_expr if ordering iss from specifyweb.specify.models import datamodel, Sptasksemaphore tree_model = datamodel.get_table(table) tasknames = [name.format(tree_model.name) for name in ("UpdateNodes{}", "BadNodes{}")] - Sptasksemaphore.objects.filter(taskname__in=tasknames).update(islocked=False) + Sptasksemaphore.objects.filter(taskname__in=tasknames).update(islocked=False) \ No newline at end of file From 45f696021c0013c544b284d29cb4a00d3b62bb0a Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 2 Feb 2026 11:29:41 -0600 Subject: [PATCH 2/2] fix: use correct key when accessing tree collection preference --- specifyweb/backend/trees/extras.py | 58 +++++++----------------------- 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/specifyweb/backend/trees/extras.py b/specifyweb/backend/trees/extras.py index ace1cdf1e3e..d236a9d50ea 100644 --- a/specifyweb/backend/trees/extras.py +++ b/specifyweb/backend/trees/extras.py @@ -215,17 +215,16 @@ def adding_node(node,collection=None, user=None): if parent.accepted_id is not None: collection_prefs_json, _, __ = app_resource.get_app_resource(collection, user, 'CollectionPreferences') - if collection_prefs_json is not None: - collection_prefs_dict = json.loads(collection_prefs_json) + collection_prefs_dict = (json.loads(collection_prefs_json) + if collection_prefs_json is not None + else dict()) treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) + synonymized = treeManagement_pref.get('synonymized', dict()) - synonymized = treeManagement_pref.get('synonymized', {}) \ - if isinstance(treeManagement_pref, dict) else {} + add_synonym_enabled = synonymized.get('sp7.allow_adding_child_to_synonymized_parent.' + node.specify_model.name, False) if isinstance(synonymized, dict) else False - add_synonym_enabled = synonymized.get(r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)', False) if isinstance(synonymized, dict) else False - - if add_synonym_enabled is True: + if add_synonym_enabled is False: raise TreeBusinessRuleException( f'Adding node "{node.fullname}" to synonymized parent "{parent.fullname}"', {"tree" : "Taxon", @@ -411,48 +410,17 @@ def synonymize(node, into, agent, user=None, collection=None): # This check can be disabled by a remote pref import specifyweb.backend.context.app_resource as app_resource - - collection_prefs_dict = {} # always defined - - res = app_resource.get_app_resource(collection, user, 'CollectionPreferences') - force_checks = (collection is None or user is None) - if res is not None: - collection_prefs_json, _, __ = res - if collection_prefs_json: - try: - collection_prefs_dict = json.loads(collection_prefs_json) or {} - except Exception: - collection_prefs_dict = {} - - import specifyweb.backend.context.app_resource as app_resource + collection_prefs_json, _, __ = app_resource.get_app_resource(collection, user, 'CollectionPreferences') + collection_prefs_dict = (json.loads(collection_prefs_json) + if collection_prefs_json is not None + else dict()) treeManagement_pref = collection_prefs_dict.get('treeManagement', {}) - if force_checks and target.children.exists(): - raise TreeBusinessRuleException( - f'Synonymizing "{node.fullname}" to "{into.fullname}" which has children', - {"tree": "Taxon", - "localizationKey": "nodeSynonimizeWithChildren", - "node": { - "id": node.id, - "rankid": node.rankid, - "fullName": node.fullname, - "children": list(node.children.values('id', 'fullname')) - }, - "parent": { - "id": into.id, - "rankid": into.rankid, - "fullName": into.fullname, - "parentid": into.parent.id, - "children": list(into.children.values('id', 'fullname')) - }} - ) - force_checks = (collection is None or user is None) - synonymized = treeManagement_pref.get('synonymized', {}) \ - if isinstance(treeManagement_pref, dict) else {} + synonymized = treeManagement_pref.get('synonymized', dict()) - add_synonym_enabled = synonymized.get(r'^sp7\.allow_adding_child_to_synonymized_parent\.' + node.specify_model.name + '=(.+)', False) if isinstance(synonymized, dict) else False + add_synonym_enabled = synonymized.get('sp7.allow_adding_child_to_synonymized_parent.' + node.specify_model.name, False) if isinstance(synonymized, dict) else False - if node.children.count() > 0 and (force_checks or add_synonym_enabled is False): + if (add_synonym_enabled == False) and node.children.count() > 0: raise TreeBusinessRuleException( f'Synonymizing node "{node.fullname}" which has children', {"tree" : "Taxon",