Skip to content

Commit

Permalink
Merge branch 'production' into issue-5154-patch
Browse files Browse the repository at this point in the history
  • Loading branch information
grantfitzsimmons authored Feb 4, 2025
2 parents deef488 + 5139ea6 commit a0c1fdf
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 80 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ RUN apt-get update \
curl \
git \
libsasl2-dev \
libsasl2-modules-gssapi-mit \
libsasl2-modules \
libldap2-dev \
libssl-dev \
libgmp-dev \
libffi-dev \
python3.8-venv \
python3.8-distutils \
python3.8-dev \
default-libmysqlclient-dev \
libmariadbclient-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Django==3.2.15
mysqlclient==2.1.1
SQLAlchemy==1.2.11
requests==2.32.2
pycryptodome==3.15.0
pycryptodome==3.21.0
PyJWT==2.3.0
django-auth-ldap==1.2.15
django-auth-ldap==1.2.17
jsonschema==3.2.0
typing-extensions==4.3.0
1 change: 1 addition & 0 deletions specifyweb/frontend/js_src/lib/components/Core/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type MenuItem = {
readonly enabled?: () => Promise<boolean> | boolean;
readonly icon: JSX.Element;
readonly name: string;
readonly onClick?: () => Promise<void>;
};

/*
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 @@ -140,10 +140,11 @@ function UserToolsColumn({
<div className="flex flex-col gap-2" key={groupName}>
<H3>{groupName}</H3>
<Ul className="flex flex-col gap-1">
{userTools.map(({ title, url, icon }) => {
{userTools.map(({ title, url, icon, onClick }) => {
const isExternalLink = isExternalUrl(url);
// Make links to another entrypoint trigger page reload
const LinkComponent = isExternalLink ? Link.NewTab : Link.Default;

return (
<li key={url}>
<LinkComponent
Expand All @@ -154,6 +155,7 @@ function UserToolsColumn({
: undefined
}
href={url}
onClick={onClick}
>
{icon}
{title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
hasTablePermission,
hasToolPermission,
} from '../Permissions/helpers';
import { clearAllCache } from '../RouterCommands/CacheBuster';
import { filterMenuItems } from './menuItemProcessing';

const rawUserTools = ensure<IR<IR<Omit<MenuItem, 'name'>>>>()({
Expand All @@ -28,6 +29,14 @@ const rawUserTools = ensure<IR<IR<Omit<MenuItem, 'name'>>>>()({
url: '/accounts/logout/',
icon: icons.logout,
enabled: () => userInformation.isauthenticated,
onClick: async () =>
clearAllCache()
.then(() => {
console.log('Cache cleared successfully.');
})
.catch((error) => {
console.error('Error occurred during cache clearing:', error);
}),
},
changePassword: {
title: userText.changePassword(),
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
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function getQueryComboBoxConditions({
* Filter values by tree definition if provided through context.
* Used for filtering Taxon values by COT tree definition.
*/
if (treeDefinition !== undefined) {
if (treeDefinition !== undefined && relatedTable === tables.Taxon) {
fields.push(
QueryFieldSpec.fromPath(tables.Taxon.name, ['definition', 'id'])
.toSpQueryField()
Expand Down
46 changes: 27 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 Expand Up @@ -550,6 +554,10 @@ export function QueryComboBox({
: fieldName === 'taxonTreeDefId'
? {
field: 'definition',
queryBuilderFieldPath: [
'definition',
'id',
],
isRelationship: true,
operation: 'in',
isNot: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAsyncState } from '../../hooks/useAsyncState';
import { toLocalUrl } from '../../utils/ajax/helpers';
import { ping } from '../../utils/ajax/ping';
import { formatUrl } from '../Router/queryString';
import { clearAllCache } from './CacheBuster';

export const switchCollection = (
navigate: SafeNavigateFunction,
Expand Down Expand Up @@ -40,7 +41,9 @@ export function SwitchCollectionCommand(): null {
method: 'POST',
body: collectionId!.toString(),
errorMode: 'dismissible',
}).then(() => globalThis.location.replace(nextUrl)),
})
.then(clearAllCache)
.then(() => globalThis.location.replace(nextUrl)),
[collectionId, nextUrl]
),
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ import type { QueryFieldFilter } from '../QueryBuilder/FieldFilter';
import { queryFieldFilters } from '../QueryBuilder/FieldFilter';
import { QueryFieldSpec } from '../QueryBuilder/fieldSpec';
import { QueryBuilder } from '../QueryBuilder/Wrapped';
import type { MappingPath } from '../WbPlanView/Mapper';
import { queryCbxExtendedSearch } from './helpers';
import { SelectRecordSets } from './SelectRecordSet';

const resourceLimit = 100;

export type QueryComboBoxFilter<SCHEMA extends AnySchema> = {
readonly field: string & (keyof CommonFields | keyof SCHEMA['fields']);
readonly queryBuilderFieldPath?: MappingPath;
readonly isRelationship: boolean;
readonly isNot: boolean;
readonly operation: QueryFieldFilter & ('between' | 'in' | 'less');
Expand Down Expand Up @@ -156,7 +158,6 @@ function testFilter<SCHEMA extends AnySchema>(
? // Cast numbers to strings
values.some((value) => {
const fieldValue = resource.get(field);

return isRelationship
? value == strictIdFromUrl(fieldValue!).toString()
: value == fieldValue;
Expand Down Expand Up @@ -395,8 +396,8 @@ const toQueryFields = <SCHEMA extends AnySchema>(
table: SpecifyTable<SCHEMA>,
filters: RA<QueryComboBoxFilter<SCHEMA>>
): RA<SpecifyResource<SpQueryField>> =>
filters.map(({ field, operation, isNot, value }) =>
QueryFieldSpec.fromPath(table.name, [field])
filters.map(({ field, queryBuilderFieldPath, operation, isNot, value }) =>
QueryFieldSpec.fromPath(table.name, queryBuilderFieldPath ?? [field])
.toSpQueryField()
.set('operStart', queryFieldFilters[operation].id)
.set('isNot', isNot)
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);
Loading

0 comments on commit a0c1fdf

Please sign in to comment.