Skip to content

Commit

Permalink
Merge branch 'production' into issue-6165
Browse files Browse the repository at this point in the history
  • Loading branch information
acwhite211 authored Feb 5, 2025
2 parents 3f85d7e + 8f3b3fb commit 736f52d
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ RUN apt-get update \
curl \
git \
libsasl2-dev \
libsasl2-modules-gssapi-mit \
libsasl2-modules \
libldap2-dev \
libssl-dev \
libgmp-dev \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,41 @@ describe('Collection Object business rules', () => {
};
overrideAjax(otherCollectionObjectTypeUrl, otherCollectionObjectType);

test('CollectionObject -> determinations: Save blocked when a determination does not belong to COT tree', 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',
getResourceApiUrl('CollectionObjectType', 2)
);
await act(async () => {
await collectionObject?.businessRuleManager?.checkField(
'collectionObjectType'
);
});
expect(result.current[0]).toStrictEqual([]);
});

test('CollectionObject -> determinations: New determinations are current by default', async () => {
const collectionObject = getBaseCollectionObject();
const determinations =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { resourcesText } from '../../localization/resources';
import { f } from '../../utils/functools';
import type { BusinessRuleResult } from './businessRules';
import {
COG_PRIMARY_KEY,
COG_TOITSELF,
CURRENT_DETERMINATION_KEY,
DETERMINATION_TAXON_KEY,
ensureSingleCollectionObjectCheck,
hasNoCurrentDetermination,
} from './businessRuleUtils';
Expand All @@ -19,7 +19,6 @@ import {
updateLoanPrep,
} from './interactionBusinessRules';
import type { SpecifyResource } from './legacyTypes';
import { fetchResource, idFromUrl } from './resource';
import { setSaveBlockers } from './saveBlockers';
import { schema } from './schema';
import type { Collection } from './specifyTable';
Expand Down Expand Up @@ -170,38 +169,31 @@ export const businessRuleDefs: MappedBusinessRuleDefs = {
},
fieldChecks: {
collectionObjectType: async (resource): Promise<undefined> => {
/*
* TEST: write tests for this
* Delete all determinations
*/
const determinations = resource.getDependentResource('determinations');
const currentDetermination = determinations?.models.find(
(determination) => determination.get('isCurrent')
if (determinations === undefined || determinations.models.length === 0)
return;

const taxons = await Promise.all(
determinations.models.map(async (det) => det.rgetPromise('taxon'))
);
const coType = await resource.rgetPromise('collectionObjectType');
const coTypeTreeDef = coType.get('taxonTreeDef');

const taxonId = idFromUrl(currentDetermination?.get('taxon') ?? '');
const COTypeID = idFromUrl(resource.get('collectionObjectType') ?? '');
if (
taxonId !== undefined &&
COTypeID !== undefined &&
currentDetermination !== undefined &&
determinations !== undefined
)
await f
.all({
fetchedTaxon: fetchResource('Taxon', taxonId),
fetchedCOType: fetchResource('CollectionObjectType', COTypeID),
})
.then(({ fetchedTaxon, fetchedCOType }) => {
const taxonTreeDefinition = fetchedTaxon.definition;
const COTypeTreeDefinition = fetchedCOType.taxonTreeDef;
// Block save when a Determination -> Taxon does not belong to the COType's tree definition
determinations.models.forEach((determination, index) => {
const taxon = taxons[index];
const taxonTreeDef = taxon?.get('definition');
const isValid =
typeof taxonTreeDef === 'string' && taxonTreeDef === coTypeTreeDef;

setSaveBlockers(
determination,
determination.specifyTable.field.taxon,
isValid ? [] : [resourcesText.invalidDeterminationTaxon()],
DETERMINATION_TAXON_KEY
);
});

if (taxonTreeDefinition !== COTypeTreeDefinition)
resource.set('determinations', []);
})
.catch((error) => {
console.error('Error fetching resources:', error);
});
return undefined;
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CURRENT_DETERMINATION_KEY = 'determination-isCurrent';
export const COG_TOITSELF = 'cog-toItself';
export const PARENTCOG_KEY = 'cog-parentCog';
export const COG_PRIMARY_KEY = 'cog-isPrimary';
export const DETERMINATION_TAXON_KEY = 'determination-Taxon';

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const isDarkMode = ({
isRedirecting,
}: PreferencesVisibilityContext): boolean => isDarkMode || isRedirecting;

const altKeyName = globalThis.navigator?.appVersion.includes('Mac')
// Navigator may not be defined in some environments, like non-browser environments
const altKeyName = typeof navigator !== 'undefined' && navigator?.userAgent?.includes('Mac')
? 'Option'
: 'Alt';

Expand Down
42 changes: 23 additions & 19 deletions specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,25 +264,29 @@ export function QueryComboBox({
(typeof typeSearch === 'object' ? typeSearch?.table : undefined) ??
field.relatedTable;

const [fetchedTreeDefinition] = useAsyncState(
React.useCallback(
async () =>
resource?.specifyTable === tables.Determination &&
resource.collection?.related?.specifyTable === tables.CollectionObject
? (resource.collection?.related as SpecifyResource<CollectionObject>)
.rgetPromise('collectionObjectType')
.then(
(
collectionObjectType:
| SpecifyResource<CollectionObjectType>
| undefined
) => collectionObjectType?.get('taxonTreeDef')
)
: undefined,
[resource, resource?.collection?.related?.get('collectionObjectType')]
),
false
);
const [fetchedTreeDefinition] = useAsyncState(
React.useCallback(async () => {
if (resource?.specifyTable === tables.Determination) {
return resource.collection?.related?.specifyTable === tables.CollectionObject
? (resource.collection?.related as SpecifyResource<CollectionObject>)
.rgetPromise('collectionObjectType')
.then(
(
collectionObjectType:
| SpecifyResource<CollectionObjectType>
| undefined
) => collectionObjectType?.get('taxonTreeDef')
)
: undefined;
} else if (resource?.specifyTable === tables.Taxon) {
const definition = resource.get('definition')
const parentDefinition = (resource?.independentResources?.parent as SpecifyResource<AnySchema>)?.get?.('definition');
return definition || parentDefinition;
}
return undefined;
}, [resource, resource?.collection?.related?.get('collectionObjectType')]),
false
);

// Tree Definition passed by a parent QCBX in the component tree
const parentTreeDefinition = React.useContext(TreeDefinitionContext);
Expand Down
1 change: 1 addition & 0 deletions specifyweb/frontend/js_src/lib/localization/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ export const commonText = createDictionary({
},
zoom: {
'en-us': 'Zoom',
'fr-fr': 'Zoom',
},
unzoom: {
'en-us': 'Unzoom',
Expand Down
4 changes: 4 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,4 +846,8 @@ export const resourcesText = createDictionary({
'en-us':
'A Consolidated Collection Object Group must have a primary Collection Object child',
},
invalidDeterminationTaxon: {
'en-us':
'Determination does not belong to the taxon tree associated with the Collection Object Type',
},
} as const);
2 changes: 1 addition & 1 deletion specifyweb/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_sa_db_url(db_name):

SPECIFY_THICK_CLIENT = os.path.expanduser(THICK_CLIENT_LOCATION)

SPECIFY_CONFIG_DIR = os.path.join(SPECIFY_THICK_CLIENT, "config")
SPECIFY_CONFIG_DIR = os.environ.get('SPECIFY_CONFIG_DIR', os.path.join(SPECIFY_THICK_CLIENT, "config"))

RO_MODE = False

Expand Down
2 changes: 2 additions & 0 deletions specifyweb/settings/specify_settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os

# Specify 7 requires the files from a Specify 6 install.
# This setting should point to a directory containing an installation
# of Specify 6 of the same version as the Specify database.
THICK_CLIENT_LOCATION = '/opt/Specify'
SPECIFY_CONFIG_DIR = os.environ.get('SPECIFY_CONFIG_DIR', os.path.join(THICK_CLIENT_LOCATION, "config"))

# Set the database name to the MySQL database you
# want to access which must be a Specify database already
Expand Down

0 comments on commit 736f52d

Please sign in to comment.