From 31f59c52cd2cb81439c7c0bfeb7bfb29e4d876fe Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 22 Jul 2024 15:41:02 -0400 Subject: [PATCH 1/8] Add business rule to validate Determination taxon with COType --- .../components/DataModel/businessRuleDefs.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index ec35f8065d4..cae062ee5c7 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -10,6 +10,7 @@ import { updateLoanPrep, } from './interactionBusinessRules'; import type { SpecifyResource } from './legacyTypes'; +import { idFromUrl } from './resource'; import { setSaveBlockers } from './saveBlockers'; import type { Collection } from './specifyTable'; import { tables } from './tables'; @@ -17,6 +18,7 @@ import type { Address, BorrowMaterial, CollectionObject, + CollectionObjectType, Determination, DNASequence, LoanPreparation, @@ -153,7 +155,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { fieldChecks: { taxon: async ( determination: SpecifyResource - ): Promise => + ): Promise => determination .rgetPromise('taxon', true) .then((taxon: SpecifyResource | null) => { @@ -165,19 +167,35 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { .then(async (accepted) => accepted === null ? taxon : getLastAccepted(accepted) ); - return taxon === null - ? { - isValid: true, - action: () => determination.set('preferredTaxon', null), + + const collectionObject = determination.collection + ?.related as SpecifyResource; + collectionObject + .rgetPromise('collectionObjectType', true) + .then((coType: SpecifyResource) => { + if (idFromUrl(coType.get('taxonTreeDef')) !== idFromUrl(taxon?.get('definition') ?? '')) { + return { + isValid: false, + reason: 'tree def not same', + }; } - : { - isValid: true, - action: async () => - determination.set( - 'preferredTaxon', - await getLastAccepted(taxon) - ), - }; + + return taxon === null + ? { + isValid: true, + action: () => determination.set('preferredTaxon', null), + } + : { + isValid: true, + action: async () => + determination.set( + 'preferredTaxon', + await getLastAccepted(taxon) + ), + }; + }); + + return undefined; }), isCurrent: async ( determination: SpecifyResource From 4fc68092f6dce6139160d3e0c5faf56ad4e29f4f Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 22 Jul 2024 19:44:37 +0000 Subject: [PATCH 2/8] Lint code with ESLint and Prettier Triggered by 31f59c52cd2cb81439c7c0bfeb7bfb29e4d876fe on branch refs/heads/issue-5111 --- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index cae062ee5c7..943cdea32e1 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -170,10 +170,13 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { const collectionObject = determination.collection ?.related as SpecifyResource; - collectionObject + collectionObject .rgetPromise('collectionObjectType', true) .then((coType: SpecifyResource) => { - if (idFromUrl(coType.get('taxonTreeDef')) !== idFromUrl(taxon?.get('definition') ?? '')) { + if ( + idFromUrl(coType.get('taxonTreeDef')) !== + idFromUrl(taxon?.get('definition') ?? '') + ) { return { isValid: false, reason: 'tree def not same', From 3bd13d2841e73919e131b24f20f7371ba607c4b5 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 22 Jul 2024 15:41:02 -0400 Subject: [PATCH 3/8] Add business rule to validate Determination taxon with COType --- .../components/DataModel/businessRuleDefs.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index ec35f8065d4..cae062ee5c7 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -10,6 +10,7 @@ import { updateLoanPrep, } from './interactionBusinessRules'; import type { SpecifyResource } from './legacyTypes'; +import { idFromUrl } from './resource'; import { setSaveBlockers } from './saveBlockers'; import type { Collection } from './specifyTable'; import { tables } from './tables'; @@ -17,6 +18,7 @@ import type { Address, BorrowMaterial, CollectionObject, + CollectionObjectType, Determination, DNASequence, LoanPreparation, @@ -153,7 +155,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { fieldChecks: { taxon: async ( determination: SpecifyResource - ): Promise => + ): Promise => determination .rgetPromise('taxon', true) .then((taxon: SpecifyResource | null) => { @@ -165,19 +167,35 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { .then(async (accepted) => accepted === null ? taxon : getLastAccepted(accepted) ); - return taxon === null - ? { - isValid: true, - action: () => determination.set('preferredTaxon', null), + + const collectionObject = determination.collection + ?.related as SpecifyResource; + collectionObject + .rgetPromise('collectionObjectType', true) + .then((coType: SpecifyResource) => { + if (idFromUrl(coType.get('taxonTreeDef')) !== idFromUrl(taxon?.get('definition') ?? '')) { + return { + isValid: false, + reason: 'tree def not same', + }; } - : { - isValid: true, - action: async () => - determination.set( - 'preferredTaxon', - await getLastAccepted(taxon) - ), - }; + + return taxon === null + ? { + isValid: true, + action: () => determination.set('preferredTaxon', null), + } + : { + isValid: true, + action: async () => + determination.set( + 'preferredTaxon', + await getLastAccepted(taxon) + ), + }; + }); + + return undefined; }), isCurrent: async ( determination: SpecifyResource From 5681a53f91da3bb54e89828489177a4fb22e832a Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 22 Jul 2024 17:21:48 -0400 Subject: [PATCH 4/8] Use save blockers --- .../components/DataModel/businessRuleDefs.ts | 61 ++++++++++++------- .../frontend/js_src/lib/localization/forms.ts | 4 ++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index cae062ee5c7..15c0f847345 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -1,3 +1,4 @@ +import { formsText } from '../../localization/forms'; import { resourcesText } from '../../localization/resources'; import type { BusinessRuleResult } from './businessRules'; import type { AnySchema, TableFields } from './helperTypes'; @@ -49,6 +50,7 @@ type MappedBusinessRuleDefs = { }; const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; +const DETERMINATION_TAXON_KEY = 'determination-taxon'; export const businessRuleDefs: MappedBusinessRuleDefs = { Address: { @@ -155,7 +157,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { fieldChecks: { taxon: async ( determination: SpecifyResource - ): Promise => + ): Promise => determination .rgetPromise('taxon', true) .then((taxon: SpecifyResource | null) => { @@ -170,32 +172,47 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { const collectionObject = determination.collection ?.related as SpecifyResource; - collectionObject + collectionObject .rgetPromise('collectionObjectType', true) .then((coType: SpecifyResource) => { - if (idFromUrl(coType.get('taxonTreeDef')) !== idFromUrl(taxon?.get('definition') ?? '')) { - return { - isValid: false, - reason: 'tree def not same', - }; + /* + * Have to set save blockers directly here to get this working. + * Since following code has to wait for above rgetPromise to resolve, returning a Promise for validation here is too slow and + * does not get captured by business rules. + */ + if ( + idFromUrl(coType.get('taxonTreeDef')) !== + idFromUrl(taxon?.get('definition') ?? '') + ) { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [formsText.invalidTree()], + DETERMINATION_TAXON_KEY + ); + } else { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [], + DETERMINATION_TAXON_KEY + ); } - - return taxon === null - ? { - isValid: true, - action: () => determination.set('preferredTaxon', null), - } - : { - isValid: true, - action: async () => - determination.set( - 'preferredTaxon', - await getLastAccepted(taxon) - ), - }; }); - return undefined; + return taxon === null + ? { + isValid: true, + action: () => determination.set('preferredTaxon', null), + } + : { + isValid: true, + action: async () => + determination.set( + 'preferredTaxon', + await getLastAccepted(taxon) + ), + }; }), isCurrent: async ( determination: SpecifyResource diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index c036fd0747d..6f707589150 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1153,4 +1153,8 @@ export const formsText = createDictionary({ 'ru-ru': 'Номер по каталогу Числовой', 'uk-ua': 'Каталожний номер Числовий', }, + invalidTree: { + 'en-us': + 'Taxon does not belong to the same tree as this Object Type', + }, } as const); From 892a2ad5a93e803385542017e860793fcb79563e Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 22 Jul 2024 21:27:18 +0000 Subject: [PATCH 5/8] Lint code with ESLint and Prettier Triggered by bc75fd066f3a432c046f2fbce15bc05cb6d43ad8 on branch refs/heads/issue-5111 --- .../lib/components/DataModel/businessRuleDefs.ts | 10 +++++----- specifyweb/frontend/js_src/lib/localization/forms.ts | 3 +-- .../frontend/js_src/lib/utils/cache/definitions.ts | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index be8b7e97a3a..d88b32fc41f 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -176,25 +176,25 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { .rgetPromise('collectionObjectType', true) .then((coType: SpecifyResource) => { /* - * Have to set save blockers directly here to get this working. + * Have to set save blockers directly here to get this working. * Since following code has to wait for above rgetPromise to resolve, returning a Promise for validation here is too slow and - * does not get captured by business rules. + * does not get captured by business rules. */ if ( - idFromUrl(coType.get('taxonTreeDef')) !== + idFromUrl(coType.get('taxonTreeDef')) === idFromUrl(taxon?.get('definition') ?? '') ) { setSaveBlockers( determination, determination.specifyTable.field.taxon, - [formsText.invalidTree()], + [], DETERMINATION_TAXON_KEY ); } else { setSaveBlockers( determination, determination.specifyTable.field.taxon, - [], + [formsText.invalidTree()], DETERMINATION_TAXON_KEY ); } diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 6f707589150..c97f9a75d6b 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1154,7 +1154,6 @@ export const formsText = createDictionary({ 'uk-ua': 'Каталожний номер Числовий', }, invalidTree: { - 'en-us': - 'Taxon does not belong to the same tree as this Object Type', + 'en-us': 'Taxon does not belong to the same tree as this Object Type', }, } as const); diff --git a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts index 5d939f461aa..59a6c0062a3 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts @@ -73,6 +73,8 @@ export type CacheDefinitions = { readonly applyAll: boolean; }; readonly tree: { + readonly [key in `definition${AnyTree['tableName']}`]: number; + } & { readonly [key in `focusPath${AnyTree['tableName']}`]: RA; } & { readonly /** Collapsed ranks in a given tree */ @@ -80,8 +82,6 @@ export type CacheDefinitions = { } & { readonly /** Open nodes in a given tree */ [key in `conformations${AnyTree['tableName']}`]: Conformations; - } & { - readonly [key in `definition${AnyTree['tableName']}`]: number; } & { readonly hideEmptyNodes: boolean; readonly isSplit: boolean; From 669fcef6ffb4f6e27c8fa4fa5b4c259191832502 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:41:53 -0400 Subject: [PATCH 6/8] Remove idFromUrl Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../js_src/lib/components/DataModel/businessRuleDefs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index d88b32fc41f..2171e10aa98 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -181,8 +181,8 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { * does not get captured by business rules. */ if ( - idFromUrl(coType.get('taxonTreeDef')) === - idFromUrl(taxon?.get('definition') ?? '') + coType.get('taxonTreeDef') === + (taxon?.get('definition') ?? '') ) { setSaveBlockers( determination, From 6caa505cb8edb622a9cfa16af874d53dbac037d8 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 24 Jul 2024 16:23:07 -0400 Subject: [PATCH 7/8] remove import --- .../components/DataModel/businessRuleDefs.ts | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 2171e10aa98..9f5500a8eb0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -11,7 +11,6 @@ import { updateLoanPrep, } from './interactionBusinessRules'; import type { SpecifyResource } from './legacyTypes'; -import { idFromUrl } from './resource'; import { setSaveBlockers } from './saveBlockers'; import type { Collection } from './specifyTable'; import { tables } from './tables'; @@ -170,35 +169,38 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { accepted === null ? taxon : getLastAccepted(accepted) ); - const collectionObject = determination.collection - ?.related as SpecifyResource; - collectionObject - .rgetPromise('collectionObjectType', true) - .then((coType: SpecifyResource) => { - /* - * Have to set save blockers directly here to get this working. - * Since following code has to wait for above rgetPromise to resolve, returning a Promise for validation here is too slow and - * does not get captured by business rules. - */ - if ( - coType.get('taxonTreeDef') === - (taxon?.get('definition') ?? '') - ) { - setSaveBlockers( - determination, - determination.specifyTable.field.taxon, - [], - DETERMINATION_TAXON_KEY - ); - } else { - setSaveBlockers( - determination, - determination.specifyTable.field.taxon, - [formsText.invalidTree()], - DETERMINATION_TAXON_KEY - ); - } - }); + const collectionObject = determination.collection?.related; + if ( + collectionObject !== undefined && + collectionObject.specifyTable.name === 'CollectionObject' + ) + (collectionObject as SpecifyResource) + .rgetPromise('collectionObjectType', true) + .then((coType: SpecifyResource) => { + /* + * Have to set save blockers directly here to get this working. + * Since following code has to wait for above rgetPromise to resolve, returning a Promise for validation here is too slow and + * does not get captured by business rules. + */ + if ( + coType.get('taxonTreeDef') === + (taxon?.get('definition') ?? '') + ) { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [], + DETERMINATION_TAXON_KEY + ); + } else { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [formsText.invalidTree()], + DETERMINATION_TAXON_KEY + ); + } + }); return taxon === null ? { From 6e71144a68e2fe6037cc2c2e3c97cbc3c5186949 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 1 Aug 2024 13:38:08 -0500 Subject: [PATCH 8/8] Add unit tests for CO -> coType Co-authored by: Sharad S <16229739+sharadsw@users.noreply.github.com> --- .../DataModel/__tests__/businessRules.test.ts | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) 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 ce4f0f489c8..2e9fdfe988a 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 @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; import { overrideAjax } from '../../../tests/ajax'; import { mockTime, requireContext } from '../../../tests/helpers'; @@ -9,7 +9,12 @@ import { getResourceApiUrl } from '../resource'; import { useSaveBlockers } from '../saveBlockers'; import { schema } from '../schema'; import { tables } from '../tables'; -import type { Taxon, TaxonTreeDefItem } from '../types'; +import type { + CollectionObjectType, + Determination, + Taxon, + TaxonTreeDefItem, +} from '../types'; mockTime(); requireContext(); @@ -49,6 +54,28 @@ describe('Borrow Material business rules', () => { }); describe('Collection Object business rules', () => { + const collectionObjectTypeUrl = getResourceApiUrl('CollectionObjectType', 1); + const collectionObjectType: Partial< + SerializedResource + > = { + id: 1, + name: 'Entomology', + taxonTreeDef: getResourceApiUrl('Taxon', 1), + resource_uri: collectionObjectTypeUrl, + }; + overrideAjax(collectionObjectTypeUrl, collectionObjectType); + + const otherTaxonId = 1; + const otherTaxon: Partial> = { + id: otherTaxonId, + isAccepted: true, + rankId: 10, + definition: getResourceApiUrl('TaxonTreeDef', 2), + resource_uri: getResourceApiUrl('Taxon', otherTaxonId), + }; + + overrideAjax(getResourceApiUrl('Taxon', otherTaxonId), otherTaxon); + const collectionObjectlId = 2; const collectionObjectUrl = getResourceApiUrl( 'CollectionObject', @@ -58,6 +85,13 @@ describe('Collection Object business rules', () => { const getBaseCollectionObject = () => new tables.CollectionObject.Resource({ id: collectionObjectlId, + collectionobjecttype: collectionObjectTypeUrl, + determinations: [ + { + taxon: getResourceApiUrl('Taxon', otherTaxonId), + preferredTaxon: getResourceApiUrl('Taxon', otherTaxonId), + } as SerializedResource, + ], resource_uri: collectionObjectUrl, }); @@ -80,6 +114,25 @@ describe('Collection Object business rules', () => { expect(collectionObject.get('collectingEvent')).toBeDefined(); }); + + test('Save blocked when CollectionObjectType of a CollectionObject does not have same tree definition as its associated Determination -> taxon', async () => { + const collectionObject = getBaseCollectionObject(); + + const determination = + collectionObject.getDependentResource('determinations')?.models[0]; + + const { result } = renderHook(() => + useSaveBlockers(determination, tables.Determination.getField('taxon')) + ); + + await act(async () => { + await determination?.businessRuleManager?.checkField('taxon'); + }); + + expect(result.current[0]).toStrictEqual([ + 'Taxon does not belong to the same tree as this Object Type', + ]); + }); }); describe('DNASequence business rules', () => {