From 3f51a4c3a84a324e4fec9304c4727087e005b79d Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 7 Apr 2026 14:59:10 -0500 Subject: [PATCH 1/4] Fix CollectionObject business rule crash on missing collectionObjectType --- .../lib/components/DataModel/businessRuleDefs.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 5b0fe4fe7d0..d6542338da6 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -181,6 +181,19 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { determinations.models.map(async (det) => det.rgetPromise('taxon')) ); const coType = await resource.rgetPromise('collectionObjectType'); + + if (coType === null) { + determinations.models.forEach((determination) => { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [], + DETERMINATION_TAXON_KEY + ); + }); + return; + } + const coTypeTreeDef = coType.get('taxonTreeDef'); // Block save when a Determination -> Taxon does not belong to the COType's tree definition From 97134533b57d4831252895ef45cabce3bcabb40d Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 7 Apr 2026 15:10:59 -0500 Subject: [PATCH 2/4] Add unit tests --- .../DataModel/__tests__/businessRules.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index 26226f396f9..1ef80c1e81d 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -194,6 +194,64 @@ describe('Collection Object business rules', () => { expect(result.current[0]).toStrictEqual([]); }); + test('CollectionObject -> determinations: Save is not blocked when collection object type is missing', async () => { + const collectionObject = getBaseCollectionObject(); + collectionObject.set('collectionObjectType', null as never, { + silent: true, + }); + + const determination = + collectionObject.getDependentResource('determinations')?.models[0]; + + const { result } = renderHook(() => + useSaveBlockers(determination, tables.Determination.getField('Taxon')) + ); + + await act(async () => { + await collectionObject?.businessRuleManager?.checkField( + 'collectionObjectType' + ); + }); + + expect(result.current[0]).toStrictEqual([]); + }); + + test('CollectionObject -> determinations: Missing collection object type clears invalid determination blockers', async () => { + const collectionObject = getBaseCollectionObject(); + collectionObject.set( + 'collectionObjectType', + getResourceApiUrl('CollectionObjectType', 1) + ); + + const determination = + collectionObject.getDependentResource('determinations')?.models[0]; + + const { result } = renderHook(() => + useSaveBlockers(determination, tables.Determination.getField('Taxon')) + ); + + await act(async () => { + await collectionObject?.businessRuleManager?.checkField( + 'collectionObjectType' + ); + }); + expect(result.current[0]).toStrictEqual([ + resourcesText.invalidDeterminationTaxon(), + ]); + + collectionObject.set('collectionObjectType', null as never, { + silent: true, + }); + + await act(async () => { + await collectionObject?.businessRuleManager?.checkField( + 'collectionObjectType' + ); + }); + + expect(result.current[0]).toStrictEqual([]); + }); + test('Newly added determinations are current by default', async () => { const collectionObject = getBaseCollectionObject(); const determinations = From a82ba0bf5c323f2063000224b4d2e6bd18ec45ae Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 7 Apr 2026 15:57:52 -0500 Subject: [PATCH 3/4] Add get_or_create_default_collection_object_type function --- .../rules/collectionobject_rules.py | 7 +++++-- specifyweb/specify/api/utils.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/businessrules/rules/collectionobject_rules.py b/specifyweb/backend/businessrules/rules/collectionobject_rules.py index 40ffec39a0c..2afcef1bc72 100644 --- a/specifyweb/backend/businessrules/rules/collectionobject_rules.py +++ b/specifyweb/backend/businessrules/rules/collectionobject_rules.py @@ -2,6 +2,7 @@ from specifyweb.backend.businessrules.exceptions import BusinessRuleException from specifyweb.backend.businessrules.utils import get_unique_catnum_across_comp_co_coll_pref +from specifyweb.specify.api.utils import get_or_create_default_collection_object_type from specifyweb.specify.models import Component @@ -10,8 +11,10 @@ def collectionobject_pre_save(co): if co.collectionmemberid is None: co.collectionmemberid = co.collection_id - if co.collectionobjecttype is None: - co.collectionobjecttype = co.collection.collectionobjecttype + if co.collectionobjecttype is None: + co.collectionobjecttype = get_or_create_default_collection_object_type( + co.collection, using=co._state.db or 'default' + ) agent = co.createdbyagent if agent is not None and agent.specifyuser is not None: diff --git a/specifyweb/specify/api/utils.py b/specifyweb/specify/api/utils.py index 1b53ff42bd7..89b17d7a6e3 100644 --- a/specifyweb/specify/api/utils.py +++ b/specifyweb/specify/api/utils.py @@ -29,6 +29,26 @@ def log_sqlalchemy_query(query): # Run in the storred_queries.execute file, in the execute function, right before the return statement, line 546 # from specifyweb.specify.utils import log_sqlalchemy_query; log_sqlalchemy_query(query) +def get_or_create_default_collection_object_type(collection: spmodels.Collection, using: str = "default"): + db = using or "default" + + if collection.collectionobjecttype is not None: + return collection.collectionobjecttype + + default_type, _ = spmodels.Collectionobjecttype.objects.using(db).get_or_create( + name=collection.discipline.name, + collection=collection, + taxontreedef_id=collection.discipline.taxontreedef_id, + ) + + type(collection).objects.using(db).filter( + pk=collection.pk, + collectionobjecttype__isnull=True, + ).update(collectionobjecttype=default_type) + collection.collectionobjecttype = default_type + + return default_type + def create_default_collection_types(apps, using="default"): db = using or "default" From 2f9d5c3d4bcb5b6791892ac4475d467de5375784 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Wed, 8 Apr 2026 10:39:25 -0500 Subject: [PATCH 4/4] fix unit tests --- .../businessrules/rules/collectionobject_rules.py | 13 +++++++++---- specifyweb/specify/api/utils.py | 12 +++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/specifyweb/backend/businessrules/rules/collectionobject_rules.py b/specifyweb/backend/businessrules/rules/collectionobject_rules.py index 2afcef1bc72..3d84088b567 100644 --- a/specifyweb/backend/businessrules/rules/collectionobject_rules.py +++ b/specifyweb/backend/businessrules/rules/collectionobject_rules.py @@ -12,9 +12,14 @@ def collectionobject_pre_save(co): co.collectionmemberid = co.collection_id if co.collectionobjecttype is None: - co.collectionobjecttype = get_or_create_default_collection_object_type( - co.collection, using=co._state.db or 'default' - ) + if co.collection.collectionobjecttype is not None: + co.collectionobjecttype = co.collection.collectionobjecttype + elif co.pk is not None: + co.collectionobjecttype = ( + get_or_create_default_collection_object_type( + co.collection, using=co._state.db or 'default' + ) + ) agent = co.createdbyagent if agent is not None and agent.specifyuser is not None: @@ -28,4 +33,4 @@ def collectionobject_pre_save(co): if contains_component_duplicates: raise BusinessRuleException( - 'Catalog Number is already in use for another Component in this collection.') \ No newline at end of file + 'Catalog Number is already in use for another Component in this collection.') diff --git a/specifyweb/specify/api/utils.py b/specifyweb/specify/api/utils.py index 89b17d7a6e3..75fdde50871 100644 --- a/specifyweb/specify/api/utils.py +++ b/specifyweb/specify/api/utils.py @@ -35,10 +35,16 @@ def get_or_create_default_collection_object_type(collection: spmodels.Collection if collection.collectionobjecttype is not None: return collection.collectionobjecttype + discipline_name = collection.discipline.name + taxon_tree_def_id = collection.discipline.taxontreedef_id + + if discipline_name is None or taxon_tree_def_id is None: + return None + default_type, _ = spmodels.Collectionobjecttype.objects.using(db).get_or_create( - name=collection.discipline.name, + name=discipline_name, collection=collection, - taxontreedef_id=collection.discipline.taxontreedef_id, + taxontreedef_id=taxon_tree_def_id, ) type(collection).objects.using(db).filter( @@ -110,4 +116,4 @@ def get_picklists(collection: spmodels.Collection, tablename: str, fieldname: st if len(collection_picklists) > 0: picklists = collection_picklists - return picklists, schemaitem \ No newline at end of file + return picklists, schemaitem