From 2800ec8ea3ac5c9b25924699c4669e515655d619 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 17 Nov 2025 10:37:31 -0500 Subject: [PATCH 01/12] Restore legacy catalog-number inheritance: handle empty values correctly and prevent parser placeholder from overriding inherited value --- specifyweb/backend/inheritance/views.py | 2 +- .../lib/components/FormFields/Field.tsx | 69 +++++++++++-------- .../lib/hooks/useParserDefaultValue.tsx | 6 +- .../js_src/lib/hooks/useResourceValue.tsx | 12 +++- 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/specifyweb/backend/inheritance/views.py b/specifyweb/backend/inheritance/views.py index 11d5ec9ead5..f3a32e8249f 100644 --- a/specifyweb/backend/inheritance/views.py +++ b/specifyweb/backend/inheritance/views.py @@ -21,7 +21,7 @@ def catalog_number_for_sibling(request: http.HttpRequest): if object_id is None: return http.JsonResponse({'error': "'id' field is required."}, status=400) - if provided_catalog_number is not None: + if provided_catalog_number not in (None, ''): return http.JsonResponse(None, safe=False) try: diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 1d1f69191a2..8546d0d78e8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -103,24 +103,11 @@ function Field({ readonly field: LiteralField | undefined; readonly parser?: Parser; }): JSX.Element { - const { value, updateValue, validationRef, parser } = useResourceValue( - resource, - field, - defaultParser - ); - - /* - * REFACTOR: consider moving this into useResoruceValue - * (it will be added to parser) - */ const isInSearchDialog = React.useContext(SearchDialogContext); const isReadOnly = React.useContext(ReadOnlyContext) || (field?.isReadOnly === true && !isInSearchDialog); - const validationAttributes = getValidationAttributes(parser); - const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); - const isNew = resource?.isNew(); const isCO = resource?.specifyTable.name === 'CollectionObject'; const isComponent = resource?.specifyTable.name === 'Component'; @@ -150,12 +137,19 @@ function Field({ 'inheritance' ); + const { value, updateValue, validationRef, parser } = useResourceValue( + resource, + field, + defaultParser + ); + const displayPrimaryCatNumberPlaceHolder = isNew === false && isCO && isPartOfCOG && isCatNumberField && - displayPrimaryCatNumberPref; + displayPrimaryCatNumberPref && + (value === null || value === ''); const displayParentCatNumberPlaceHolder = isNew === false && @@ -164,6 +158,13 @@ function Field({ isCatNumberField && displayParentCatNumberPref; + /* + * REFACTOR: consider moving this into useResoruceValue + * (it will be added to parser) + */ + const validationAttributes = getValidationAttributes(parser); + const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); + const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState< string | null >(null); @@ -172,6 +173,14 @@ function Field({ string | null >(null); + const normalizeCatValue = (val: string | null | undefined): string | null => + isCO && + isCatNumberField && + displayPrimaryCatNumberPref && + (val === '' || val === undefined) + ? null + : val ?? null; + React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { ajax('/inheritance/catalog_number_for_sibling/', { @@ -204,22 +213,26 @@ function Field({ displayParentCatNumberPlaceHolder, ]); + const customPlaceholder = + displayPrimaryCatNumberPlaceHolder && + typeof primaryCatalogNumber === 'string' + ? primaryCatalogNumber + : displayParentCatNumberPlaceHolder && + typeof parentCatalogNumber === 'string' + ? parentCatalogNumber + : undefined; + + const { placeholder: parserPlaceholder, ...restValidationAttributes } = + validationAttributes; + return ( updateValue(target.value) + isReadOnly + ? undefined + : ({ target }): void => updateValue(normalizeCatValue(target.value)) } - onValueChange={(value): void => updateValue(value, false)} + onValueChange={(value): void => updateValue(normalizeCatValue(value), false)} /* * Update data model value before onBlur, as onBlur fires after onSubmit * if form is submitted using the ENTER key @@ -241,7 +256,7 @@ function Field({ * field is blurred, unless user tried to paste a date (see definition * of Input.Generic) */ - updateValue(input.value, event.type === 'paste'); + updateValue(normalizeCatValue(input.value), event.type === 'paste'); }} /> ); diff --git a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx index 6a344be433f..9135acb1bae 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx @@ -27,10 +27,12 @@ import { parseAnyDate } from '../utils/relativeDate'; export function useParserDefaultValue( resource: SpecifyResource | undefined, field: LiteralField | Relationship | undefined, - parser: Parser + parser: Parser, + suppressDefaultValue = false ) { React.useLayoutEffect(() => { - if (field === undefined || resource === undefined) return; + if (suppressDefaultValue || field === undefined || resource === undefined) + return; /* * Don't auto set numeric to "0" or boolean fields to false, unless it is the default value * in the form definition diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 6f09ec07c02..afd901b3bfd 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -46,7 +46,7 @@ export function useResourceValue< field: LiteralField | Relationship | undefined, // Default parser is usually coming from the form definition defaultParser: Parser | undefined, - trim?: boolean + trimOrOptions?: boolean | { readonly trim?: boolean; readonly suppressDefaultValue?: boolean } ): { readonly value: T | undefined; readonly updateValue: (newValue: T, reportErrors?: boolean) => void; @@ -56,9 +56,17 @@ export function useResourceValue< readonly setValidation: (message: RA | string) => void; readonly parser: Parser; } { + const { trim, suppressDefaultValue } = + typeof trimOrOptions === 'object' + ? { + trim: trimOrOptions.trim, + suppressDefaultValue: trimOrOptions.suppressDefaultValue ?? false, + } + : { trim: trimOrOptions, suppressDefaultValue: false }; + const parser = useParser(field, resource, defaultParser); - useParserDefaultValue(resource, field, parser); + useParserDefaultValue(resource, field, parser, suppressDefaultValue); const { inputRef, validationRef, setValidation } = useFieldValidation( resource, From ae758c03ca8e67c2dd22cc54cca694e22ac381c4 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 17 Nov 2025 11:21:24 -0500 Subject: [PATCH 02/12] restrict COG inheritance only for consolidated COGs --- specifyweb/backend/inheritance/api.py | 2 +- .../tests/test_catalog_number_for_sibling.py | 33 +++++++++++++------ specifyweb/backend/inheritance/views.py | 7 ++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/specifyweb/backend/inheritance/api.py b/specifyweb/backend/inheritance/api.py index 6e622d94a9e..c573bd99e6f 100644 --- a/specifyweb/backend/inheritance/api.py +++ b/specifyweb/backend/inheritance/api.py @@ -51,7 +51,7 @@ def cog_inheritance_post_query_processing(query, tableid, field_specs, collectio result = list(result) if result[catalog_number_field_index] is None or result[catalog_number_field_index] == '': cojo = Collectionobjectgroupjoin.objects.filter(childco_id=result[0]).first() - if cojo: + if cojo and getattr(getattr(cojo.parentcog, 'cogtype', None), 'type', None) == 'Consolidated': primary_cojo = Collectionobjectgroupjoin.objects.filter( parentcog=cojo.parentcog, isprimary=True).first() if primary_cojo: diff --git a/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py b/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py index 26e44cf4863..4cfd90a0c76 100644 --- a/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py +++ b/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py @@ -59,9 +59,9 @@ def test_simple_co_catalognumber(self): def test_self_is_primary(self): # In this case, the provided CO is itself primary. - self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) - self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=True) + self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) co = self.collectionobjects[0] response = self.c.post( @@ -75,9 +75,9 @@ def test_self_is_primary(self): def test_other_is_primary(self): - self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) - self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=True) + self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) for co in self.collectionobjects[1:3]: response = self.c.post( @@ -91,9 +91,9 @@ def test_other_is_primary(self): def test_none_is_primary(self): # Not sure if this is ever possible, but code handles it. - self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=False) - self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) for co in self.collectionobjects[1:3]: response = self.c.post( @@ -103,4 +103,17 @@ def test_none_is_primary(self): ) self._assertStatusCodeEqual(response, 200) - self.assertEqual(json.loads(response.content.decode()), None) \ No newline at end of file + self.assertEqual(json.loads(response.content.decode()), None) + + def test_discrete_type_returns_none(self): + self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) + self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) + + response = self.c.post( + "/inheritance/catalog_number_for_sibling/", + json.dumps(dict(id=self.collectionobjects[1].id)), + content_type="application/json" + ) + + self._assertStatusCodeEqual(response, 200) + self.assertEqual(json.loads(response.content.decode()), None) diff --git a/specifyweb/backend/inheritance/views.py b/specifyweb/backend/inheritance/views.py index f3a32e8249f..adafa4d6169 100644 --- a/specifyweb/backend/inheritance/views.py +++ b/specifyweb/backend/inheritance/views.py @@ -35,6 +35,13 @@ def catalog_number_for_sibling(request: http.HttpRequest): parent_cog_id = requesting_cojo['parentcog_id'] + parent_cog = models.Collectionobjectgroup.objects.select_related( + 'cogtype' + ).filter(id=parent_cog_id).first() + + if parent_cog is None or parent_cog.cogtype is None or parent_cog.cogtype.type != 'Consolidated': + return http.JsonResponse(None, safe=False) + primary_cojo = models.Collectionobjectgroupjoin.objects.filter( parentcog_id=parent_cog_id, isprimary=True From 84185fb848ddc0474c0900273da513f239797c3a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 17 Nov 2025 16:43:48 +0000 Subject: [PATCH 03/12] Lint code with ESLint and Prettier Triggered by ae758c03ca8e67c2dd22cc54cca694e22ac381c4 on branch refs/heads/issue-7537 --- .../frontend/js_src/lib/components/FormFields/Field.tsx | 6 +++--- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 8546d0d78e8..dea660f9ece 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -173,13 +173,13 @@ function Field({ string | null >(null); - const normalizeCatValue = (val: string | null | undefined): string | null => + const normalizeCatValue = (value_: string | null | undefined): string | null => isCO && isCatNumberField && displayPrimaryCatNumberPref && - (val === '' || val === undefined) + (value_ === '' || value_ === undefined) ? null - : val ?? null; + : value_ ?? null; React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index afd901b3bfd..0a6c06b4465 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; -import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; +import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource @@ -46,7 +46,9 @@ export function useResourceValue< field: LiteralField | Relationship | undefined, // Default parser is usually coming from the form definition defaultParser: Parser | undefined, - trimOrOptions?: boolean | { readonly trim?: boolean; readonly suppressDefaultValue?: boolean } + trimOrOptions?: + | boolean + | { readonly trim?: boolean; readonly suppressDefaultValue?: boolean } ): { readonly value: T | undefined; readonly updateValue: (newValue: T, reportErrors?: boolean) => void; From 6136841c25d5ec2386407da3b1c8b767ef32cbc0 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 18 Nov 2025 23:35:40 -0500 Subject: [PATCH 04/12] removed additional logic --- .../lib/components/FormFields/Field.tsx | 71 +++++++++++++++++++ .../lib/hooks/useParserDefaultValue.tsx | 7 ++ .../js_src/lib/hooks/useResourceValue.tsx | 8 +++ 3 files changed, 86 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index dea660f9ece..9b1fc2b8690 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -103,6 +103,7 @@ function Field({ readonly field: LiteralField | undefined; readonly parser?: Parser; }): JSX.Element { +<<<<<<< HEAD const isInSearchDialog = React.useContext(SearchDialogContext); const isReadOnly = React.useContext(ReadOnlyContext) || @@ -137,12 +138,15 @@ function Field({ 'inheritance' ); +======= +>>>>>>> e4326dfce0 (removed additional logic) const { value, updateValue, validationRef, parser } = useResourceValue( resource, field, defaultParser ); +<<<<<<< HEAD const displayPrimaryCatNumberPlaceHolder = isNew === false && isCO && @@ -158,6 +162,8 @@ function Field({ isCatNumberField && displayParentCatNumberPref; +======= +>>>>>>> e4326dfce0 (removed additional logic) /* * REFACTOR: consider moving this into useResoruceValue * (it will be added to parser) @@ -165,6 +171,7 @@ function Field({ const validationAttributes = getValidationAttributes(parser); const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); +<<<<<<< HEAD const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState< string | null >(null); @@ -180,6 +187,70 @@ function Field({ (value_ === '' || value_ === undefined) ? null : value_ ?? null; +======= + const validationAttributes = getValidationAttributes(parser); + const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); + + const isNew = resource?.isNew(); + const isCO = resource?.specifyTable.name === 'CollectionObject'; + const isComponent = resource?.specifyTable.name === 'Component'; + + const isPartOfCOG = isCO + ? resource?.get('cojo') !== null && resource?.get('cojo') !== undefined + : false; + + const hasCOParent = isComponent + ? resource.get('collectionObject') !== null && + resource.get('collectionObject') !== undefined + : false; + + const isCatNumberField = field?.name === 'catalogNumber'; + + // Check if collection pref wants to inherit primary cat num for empty CO cat num sibilings inside of a COG + const [displayPrimaryCatNumberPref] = collectionPreferences.use( + 'catalogNumberInheritance', + 'behavior', + 'inheritance' + ); + + // Check if collection pref wants to inherit parent cat num for empty CO cat num children + const [displayParentCatNumberPref] = collectionPreferences.use( + 'catalogNumberParentInheritance', + 'behavior', + 'inheritance' + ); + + const displayPrimaryCatNumberPlaceHolder = + isNew === false && + isCO && + isPartOfCOG && + isCatNumberField && + displayPrimaryCatNumberPref && + (value === null || value === ''); + + const displayParentCatNumberPlaceHolder = + isNew === false && + isComponent && + hasCOParent && + isCatNumberField && + displayParentCatNumberPref; + + const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState< + string | null + >(null); + + const [parentCatalogNumber, setParentCatalogNumber] = React.useState< + string | null + >(null); + + const normalizeCatValue = (val: string | null | undefined): string | null => + isCO && + isCatNumberField && + displayPrimaryCatNumberPref && + (val === '' || val === undefined) + ? null + : val ?? null; +>>>>>>> e4326dfce0 (removed additional logic) React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { diff --git a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx index 9135acb1bae..fe0f2cb39ea 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx @@ -27,12 +27,19 @@ import { parseAnyDate } from '../utils/relativeDate'; export function useParserDefaultValue( resource: SpecifyResource | undefined, field: LiteralField | Relationship | undefined, +<<<<<<< HEAD parser: Parser, suppressDefaultValue = false ) { React.useLayoutEffect(() => { if (suppressDefaultValue || field === undefined || resource === undefined) return; +======= + parser: Parser +) { + React.useLayoutEffect(() => { + if (field === undefined || resource === undefined) return; +>>>>>>> e4326dfce0 (removed additional logic) /* * Don't auto set numeric to "0" or boolean fields to false, unless it is the default value * in the form definition diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 0a6c06b4465..1803d3ac5d7 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -46,9 +46,13 @@ export function useResourceValue< field: LiteralField | Relationship | undefined, // Default parser is usually coming from the form definition defaultParser: Parser | undefined, +<<<<<<< HEAD trimOrOptions?: | boolean | { readonly trim?: boolean; readonly suppressDefaultValue?: boolean } +======= + trim?: boolean +>>>>>>> e4326dfce0 (removed additional logic) ): { readonly value: T | undefined; readonly updateValue: (newValue: T, reportErrors?: boolean) => void; @@ -68,7 +72,11 @@ export function useResourceValue< const parser = useParser(field, resource, defaultParser); +<<<<<<< HEAD useParserDefaultValue(resource, field, parser, suppressDefaultValue); +======= + useParserDefaultValue(resource, field, parser); +>>>>>>> e4326dfce0 (removed additional logic) const { inputRef, validationRef, setValidation } = useFieldValidation( resource, From 98c9ad5347b7c0c8338a7cc0218cca27767bf9d9 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 18 Nov 2025 23:44:40 -0500 Subject: [PATCH 05/12] removed additional inheritance logic --- specifyweb/backend/inheritance/api.py | 2 +- .../tests/test_catalog_number_for_sibling.py | 33 +++----- specifyweb/backend/inheritance/views.py | 9 +-- .../lib/components/FormFields/Field.tsx | 79 +------------------ .../lib/hooks/useParserDefaultValue.tsx | 9 --- .../js_src/lib/hooks/useResourceValue.tsx | 20 +---- 6 files changed, 17 insertions(+), 135 deletions(-) diff --git a/specifyweb/backend/inheritance/api.py b/specifyweb/backend/inheritance/api.py index c573bd99e6f..6e622d94a9e 100644 --- a/specifyweb/backend/inheritance/api.py +++ b/specifyweb/backend/inheritance/api.py @@ -51,7 +51,7 @@ def cog_inheritance_post_query_processing(query, tableid, field_specs, collectio result = list(result) if result[catalog_number_field_index] is None or result[catalog_number_field_index] == '': cojo = Collectionobjectgroupjoin.objects.filter(childco_id=result[0]).first() - if cojo and getattr(getattr(cojo.parentcog, 'cogtype', None), 'type', None) == 'Consolidated': + if cojo: primary_cojo = Collectionobjectgroupjoin.objects.filter( parentcog=cojo.parentcog, isprimary=True).first() if primary_cojo: diff --git a/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py b/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py index 4cfd90a0c76..26e44cf4863 100644 --- a/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py +++ b/specifyweb/backend/inheritance/tests/test_catalog_number_for_sibling.py @@ -59,9 +59,9 @@ def test_simple_co_catalognumber(self): def test_self_is_primary(self): # In this case, the provided CO is itself primary. - self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=True) - self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) + self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) co = self.collectionobjects[0] response = self.c.post( @@ -75,9 +75,9 @@ def test_self_is_primary(self): def test_other_is_primary(self): - self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=True) - self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) + self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) for co in self.collectionobjects[1:3]: response = self.c.post( @@ -91,9 +91,9 @@ def test_other_is_primary(self): def test_none_is_primary(self): # Not sure if this is ever possible, but code handles it. - self._link_co_cog(self.collectionobjects[0], self.test_cog_consolidated, isprimary=False) - self._link_co_cog(self.collectionobjects[1], self.test_cog_consolidated, isprimary=False) - self._link_co_cog(self.collectionobjects[2], self.test_cog_consolidated, isprimary=False) + self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) + self._link_co_cog(self.collectionobjects[2], self.test_cog_discrete, isprimary=False) for co in self.collectionobjects[1:3]: response = self.c.post( @@ -103,17 +103,4 @@ def test_none_is_primary(self): ) self._assertStatusCodeEqual(response, 200) - self.assertEqual(json.loads(response.content.decode()), None) - - def test_discrete_type_returns_none(self): - self._link_co_cog(self.collectionobjects[0], self.test_cog_discrete, isprimary=True) - self._link_co_cog(self.collectionobjects[1], self.test_cog_discrete, isprimary=False) - - response = self.c.post( - "/inheritance/catalog_number_for_sibling/", - json.dumps(dict(id=self.collectionobjects[1].id)), - content_type="application/json" - ) - - self._assertStatusCodeEqual(response, 200) - self.assertEqual(json.loads(response.content.decode()), None) + self.assertEqual(json.loads(response.content.decode()), None) \ No newline at end of file diff --git a/specifyweb/backend/inheritance/views.py b/specifyweb/backend/inheritance/views.py index adafa4d6169..11d5ec9ead5 100644 --- a/specifyweb/backend/inheritance/views.py +++ b/specifyweb/backend/inheritance/views.py @@ -21,7 +21,7 @@ def catalog_number_for_sibling(request: http.HttpRequest): if object_id is None: return http.JsonResponse({'error': "'id' field is required."}, status=400) - if provided_catalog_number not in (None, ''): + if provided_catalog_number is not None: return http.JsonResponse(None, safe=False) try: @@ -35,13 +35,6 @@ def catalog_number_for_sibling(request: http.HttpRequest): parent_cog_id = requesting_cojo['parentcog_id'] - parent_cog = models.Collectionobjectgroup.objects.select_related( - 'cogtype' - ).filter(id=parent_cog_id).first() - - if parent_cog is None or parent_cog.cogtype is None or parent_cog.cogtype.type != 'Consolidated': - return http.JsonResponse(None, safe=False) - primary_cojo = models.Collectionobjectgroupjoin.objects.filter( parentcog_id=parent_cog_id, isprimary=True diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 9b1fc2b8690..7cb0cfcde05 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -103,91 +103,21 @@ function Field({ readonly field: LiteralField | undefined; readonly parser?: Parser; }): JSX.Element { -<<<<<<< HEAD - const isInSearchDialog = React.useContext(SearchDialogContext); - const isReadOnly = - React.useContext(ReadOnlyContext) || - (field?.isReadOnly === true && !isInSearchDialog); - - const isNew = resource?.isNew(); - const isCO = resource?.specifyTable.name === 'CollectionObject'; - const isComponent = resource?.specifyTable.name === 'Component'; - - const isPartOfCOG = isCO - ? resource?.get('cojo') !== null && resource?.get('cojo') !== undefined - : false; - - const hasCOParent = isComponent - ? resource.get('collectionObject') !== null && - resource.get('collectionObject') !== undefined - : false; - - const isCatNumberField = field?.name === 'catalogNumber'; - - // Check if collection pref wants to inherit primary cat num for empty CO cat num sibilings inside of a COG - const [displayPrimaryCatNumberPref] = collectionPreferences.use( - 'catalogNumberInheritance', - 'behavior', - 'inheritance' - ); - - // Check if collection pref wants to inherit parent cat num for empty CO cat num children - const [displayParentCatNumberPref] = collectionPreferences.use( - 'catalogNumberParentInheritance', - 'behavior', - 'inheritance' - ); - -======= ->>>>>>> e4326dfce0 (removed additional logic) const { value, updateValue, validationRef, parser } = useResourceValue( resource, field, defaultParser ); -<<<<<<< HEAD - const displayPrimaryCatNumberPlaceHolder = - isNew === false && - isCO && - isPartOfCOG && - isCatNumberField && - displayPrimaryCatNumberPref && - (value === null || value === ''); - - const displayParentCatNumberPlaceHolder = - isNew === false && - isComponent && - hasCOParent && - isCatNumberField && - displayParentCatNumberPref; - -======= ->>>>>>> e4326dfce0 (removed additional logic) /* * REFACTOR: consider moving this into useResoruceValue * (it will be added to parser) */ - const validationAttributes = getValidationAttributes(parser); - const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); - -<<<<<<< HEAD - const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState< - string | null - >(null); - - const [parentCatalogNumber, setParentCatalogNumber] = React.useState< - string | null - >(null); + const isInSearchDialog = React.useContext(SearchDialogContext); + const isReadOnly = + React.useContext(ReadOnlyContext) || + (field?.isReadOnly === true && !isInSearchDialog); - const normalizeCatValue = (value_: string | null | undefined): string | null => - isCO && - isCatNumberField && - displayPrimaryCatNumberPref && - (value_ === '' || value_ === undefined) - ? null - : value_ ?? null; -======= const validationAttributes = getValidationAttributes(parser); const rightAlignClassName = useRightAlignClassName(parser.type, isReadOnly); @@ -250,7 +180,6 @@ function Field({ (val === '' || val === undefined) ? null : val ?? null; ->>>>>>> e4326dfce0 (removed additional logic) React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { diff --git a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx index fe0f2cb39ea..6a344be433f 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx @@ -27,19 +27,10 @@ import { parseAnyDate } from '../utils/relativeDate'; export function useParserDefaultValue( resource: SpecifyResource | undefined, field: LiteralField | Relationship | undefined, -<<<<<<< HEAD - parser: Parser, - suppressDefaultValue = false -) { - React.useLayoutEffect(() => { - if (suppressDefaultValue || field === undefined || resource === undefined) - return; -======= parser: Parser ) { React.useLayoutEffect(() => { if (field === undefined || resource === undefined) return; ->>>>>>> e4326dfce0 (removed additional logic) /* * Don't auto set numeric to "0" or boolean fields to false, unless it is the default value * in the form definition diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 1803d3ac5d7..6f09ec07c02 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; +import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; -import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource @@ -46,13 +46,7 @@ export function useResourceValue< field: LiteralField | Relationship | undefined, // Default parser is usually coming from the form definition defaultParser: Parser | undefined, -<<<<<<< HEAD - trimOrOptions?: - | boolean - | { readonly trim?: boolean; readonly suppressDefaultValue?: boolean } -======= trim?: boolean ->>>>>>> e4326dfce0 (removed additional logic) ): { readonly value: T | undefined; readonly updateValue: (newValue: T, reportErrors?: boolean) => void; @@ -62,21 +56,9 @@ export function useResourceValue< readonly setValidation: (message: RA | string) => void; readonly parser: Parser; } { - const { trim, suppressDefaultValue } = - typeof trimOrOptions === 'object' - ? { - trim: trimOrOptions.trim, - suppressDefaultValue: trimOrOptions.suppressDefaultValue ?? false, - } - : { trim: trimOrOptions, suppressDefaultValue: false }; - const parser = useParser(field, resource, defaultParser); -<<<<<<< HEAD - useParserDefaultValue(resource, field, parser, suppressDefaultValue); -======= useParserDefaultValue(resource, field, parser); ->>>>>>> e4326dfce0 (removed additional logic) const { inputRef, validationRef, setValidation } = useFieldValidation( resource, From ff869c3365b13495d9ba8bde8d295615a3542a5c Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 19 Nov 2025 04:49:01 +0000 Subject: [PATCH 06/12] Lint code with ESLint and Prettier Triggered by 98c9ad5347b7c0c8338a7cc0218cca27767bf9d9 on branch refs/heads/issue-7537 --- .../frontend/js_src/lib/components/FormFields/Field.tsx | 6 +++--- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 7cb0cfcde05..3a4a26b8b39 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -173,13 +173,13 @@ function Field({ string | null >(null); - const normalizeCatValue = (val: string | null | undefined): string | null => + const normalizeCatValue = (value_: string | null | undefined): string | null => isCO && isCatNumberField && displayPrimaryCatNumberPref && - (val === '' || val === undefined) + (value_ === '' || value_ === undefined) ? null - : val ?? null; + : value_ ?? null; React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 6f09ec07c02..cd1cdabf53d 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; -import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; +import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource From d3eca1c2ae8c093263051252982ff88d4ec7cd4f Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 18 Nov 2025 23:58:03 -0500 Subject: [PATCH 07/12] Remove duplicate import of useParserDefaultValue Removed redundant import of useParserDefaultValue. --- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index cd1cdabf53d..53ce5304d79 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -10,11 +10,11 @@ import type { } from '../components/DataModel/specifyField'; import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; +import { useParserDefaultValue } from './useParserDefaultValue'; import type { RA } from '../utils/types'; import { useParser } from './resource'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; -import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource From fee6bd3616977362eed7f99f1f90582d7fe2447d Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 18 Nov 2025 23:58:41 -0500 Subject: [PATCH 08/12] Import useParserDefaultValue in useResourceValue --- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 53ce5304d79..6f09ec07c02 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -10,9 +10,9 @@ import type { } from '../components/DataModel/specifyField'; import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; -import { useParserDefaultValue } from './useParserDefaultValue'; import type { RA } from '../utils/types'; import { useParser } from './resource'; +import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; From 1ce0d895071b77678ad30ef10eafc75813591d90 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 19 Nov 2025 05:02:32 +0000 Subject: [PATCH 09/12] Lint code with ESLint and Prettier Triggered by fee6bd3616977362eed7f99f1f90582d7fe2447d on branch refs/heads/issue-7537 --- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 6f09ec07c02..cd1cdabf53d 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; -import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; +import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource From 7442494b62ec2a4d044628f384c22566668518ea Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 19 Nov 2025 00:07:47 -0500 Subject: [PATCH 10/12] removed unused imports --- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index cd1cdabf53d..6f09ec07c02 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; +import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; -import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource From 2246b0e1811aa7c68bdf2992a3984a15b608a989 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 19 Nov 2025 05:11:41 +0000 Subject: [PATCH 11/12] Lint code with ESLint and Prettier Triggered by 7442494b62ec2a4d044628f384c22566668518ea on branch refs/heads/issue-7537 --- specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx index 6f09ec07c02..cd1cdabf53d 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useResourceValue.tsx @@ -12,9 +12,9 @@ import type { Input } from '../components/Forms/validationHelpers'; import type { Parser } from '../utils/parser/definitions'; import type { RA } from '../utils/types'; import { useParser } from './resource'; -import { useParserDefaultValue } from './useParserDefaultValue'; import { useFieldParser } from './useFieldParser'; import { useFieldValidation } from './useFieldValidation'; +import { useParserDefaultValue } from './useParserDefaultValue'; /** * A hook to integrate an Input with a field on a Backbone resource From 228fe8a763adbb315ce761334077116bcfa0d4ed Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 19 Nov 2025 10:00:32 -0500 Subject: [PATCH 12/12] Remove Obsolete normalizeCatValue --- .../js_src/lib/components/FormFields/Field.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 3a4a26b8b39..c9bf11d6e51 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -155,8 +155,7 @@ function Field({ isCO && isPartOfCOG && isCatNumberField && - displayPrimaryCatNumberPref && - (value === null || value === ''); + displayPrimaryCatNumberPref; const displayParentCatNumberPlaceHolder = isNew === false && @@ -173,14 +172,6 @@ function Field({ string | null >(null); - const normalizeCatValue = (value_: string | null | undefined): string | null => - isCO && - isCatNumberField && - displayPrimaryCatNumberPref && - (value_ === '' || value_ === undefined) - ? null - : value_ ?? null; - React.useEffect(() => { if (resource && displayPrimaryCatNumberPlaceHolder) { ajax('/inheritance/catalog_number_for_sibling/', { @@ -242,9 +233,9 @@ function Field({ onBlur={ isReadOnly ? undefined - : ({ target }): void => updateValue(normalizeCatValue(target.value)) + : ({ target }): void => updateValue(target.value) } - onValueChange={(value): void => updateValue(normalizeCatValue(value), false)} + onValueChange={(value): void => updateValue(value, false)} /* * Update data model value before onBlur, as onBlur fires after onSubmit * if form is submitted using the ENTER key @@ -256,7 +247,7 @@ function Field({ * field is blurred, unless user tried to paste a date (see definition * of Input.Generic) */ - updateValue(normalizeCatValue(input.value), event.type === 'paste'); + updateValue(input.value, event.type === 'paste'); }} /> );