Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/production' into issue-922
Browse files Browse the repository at this point in the history
  • Loading branch information
CarolineDenis committed May 6, 2024
2 parents 7261d4a + 71ad206 commit e76e4ff
Show file tree
Hide file tree
Showing 29 changed files with 592 additions and 376 deletions.
Expand Up @@ -250,6 +250,20 @@ describe('treeBusinessRules', () => {
resource_uri: '/api/specify/taxontreedefitem/2/',
};

const subSpeciesResponse: Partial<SerializedResource<TaxonTreeDefItem>> = {
id: 22,
fullNameSeparator: ' ',
isEnforced: false,
isInFullName: true,
name: 'Subspecies',
rankId: 230,
title: null,
version: 0,
parent: '/api/specify/taxontreedefitem/2/',
treeDef: '/api/specify/taxontreedef/1/',
resource_uri: '/api/specify/taxontreedefitem/22/',
};

const oxyrinchusFullNameResponse = 'Acipenser oxyrinchus';

overrideAjax('/api/specify/taxon/2/', animaliaResponse);
Expand All @@ -258,10 +272,19 @@ describe('treeBusinessRules', () => {
overrideAjax('/api/specify/taxon/5/', oxyrinchusSubSpeciesResponse);
overrideAjax('/api/specify/taxontreedefitem/9/', genusResponse);
overrideAjax('/api/specify/taxontreedefitem/2/', speciesResponse);
overrideAjax('/api/specify/taxontreedefitem/22/', subSpeciesResponse);
overrideAjax(
'/api/specify_tree/taxon/3/predict_fullname/?name=oxyrinchus&treedefitemid=2',
oxyrinchusFullNameResponse
);
overrideAjax('/api/specify/taxon/?limit=1&parent=4&orderby=rankid', {
objects: [oxyrinchusSubSpeciesResponse],
meta: {
limit: 1,
offset: 0,
total_count: 1,
},
});

test('fullName being set', async () => {
const oxyrinchus = new tables.Taxon.Resource({
Expand Down
Expand Up @@ -8,7 +8,12 @@ import { isTreeResource } from '../InitialContext/treeRanks';
import type { BusinessRuleDefs } from './businessRuleDefs';
import { businessRuleDefs } from './businessRuleDefs';
import { backboneFieldSeparator, djangoLookupSeparator } from './helpers';
import type { AnySchema, AnyTree, CommonFields } from './helperTypes';
import type {
AnySchema,
AnyTree,
CommonFields,
TableFields,
} from './helperTypes';
import type { SpecifyResource } from './legacyTypes';
import {
getFieldBlockerKey,
Expand Down Expand Up @@ -77,7 +82,7 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
isTreeResource(this.resource as SpecifyResource<AnySchema>)
? treeBusinessRules(
this.resource as SpecifyResource<AnyTree>,
processedFieldName
processedFieldName as TableFields<AnyTree>
)
: Promise.resolve({ isValid: true }),
];
Expand Down
@@ -1,13 +1,18 @@
import { treeText } from '../../localization/tree';
import { ajax } from '../../utils/ajax';
import { f } from '../../utils/functools';
import { fetchPossibleRanks } from '../PickLists/TreeLevelPickList';
import { formatUrl } from '../Router/queryString';
import type { BusinessRuleResult } from './businessRules';
import type { AnyTree, FilterTablesByEndsWith } from './helperTypes';
import type {
AnyTree,
FilterTablesByEndsWith,
TableFields,
} from './helperTypes';
import type { SpecifyResource } from './legacyTypes';

// eslint-disable-next-line unicorn/prevent-abbreviations
type TreeDefItem<TREE extends AnyTree> =
export type TreeDefItem<TREE extends AnyTree> =
FilterTablesByEndsWith<`${TREE['tableName']}TreeDefItem`>;

export const initializeTreeRecord = (
Expand All @@ -19,15 +24,27 @@ export const initializeTreeRecord = (

export const treeBusinessRules = async (
resource: SpecifyResource<AnyTree>,
fieldName: string
fieldName: TableFields<AnyTree>
): Promise<BusinessRuleResult | undefined> =>
getRelatedTreeTables(resource).then(async ({ parent, definitionItem }) => {
if (parent === undefined) return undefined;

const parentDefItem = ((await parent.rgetPromise('definitionItem')) ??
undefined) as SpecifyResource<TreeDefItem<AnyTree>> | undefined;

const possibleRanks =
parentDefItem === undefined
? undefined
: await fetchPossibleRanks(resource, parentDefItem.get('rankId'));

const hasBadTreeStrcuture =
parent.id === resource.id ||
definitionItem === undefined ||
parent.get('rankId') >= definitionItem.get('rankId');
parent.get('rankId') >= definitionItem.get('rankId') ||
(possibleRanks !== undefined &&
!possibleRanks
.map(({ resource_uri }) => resource_uri)
.includes(definitionItem.get('resource_uri')));

if (!hasBadTreeStrcuture && (resource.get('name').length ?? 0) === 0)
return {
Expand Down
13 changes: 9 additions & 4 deletions specifyweb/frontend/js_src/lib/components/FormEditor/Create.tsx
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { useOutletContext } from 'react-router';
import { useNavigate } from 'react-router-dom';
import type { LocalizedString } from 'typesafe-i18n';
import { useAsyncState } from '../../hooks/useAsyncState';

import { useBooleanState } from '../../hooks/useBooleanState';
import { useId } from '../../hooks/useId';
Expand Down Expand Up @@ -192,10 +193,14 @@ export function PreviewView({
readonly onSelect: () => void;
}): JSX.Element {
const resource = React.useMemo(() => new table.Resource(), [table]);
const viewDefinition = React.useMemo(
() => parseViewDefinition(view, 'form', 'edit', table),
[view, table]
);
const viewDefinition = useAsyncState(
React.useCallback(
async () => parseViewDefinition(view, 'form', 'edit', table),
[view, table]
),
true
)[0];

return (
<Dialog
buttons={
Expand Down
58 changes: 28 additions & 30 deletions specifyweb/frontend/js_src/lib/components/FormEditor/Editor.tsx
Expand Up @@ -295,39 +295,37 @@ function FormPreview({
ViewDescription | undefined
>(undefined);
React.useEffect(() => {
try {
const parsed = parseViewDefinition(
{
altviews: {
[table.name]: {
default: 'true',
mode: 'edit',
name: '',
viewdef: table.name,
},
parseViewDefinition(
{
altviews: {
[table.name]: {
default: 'true',
mode: 'edit',
name: '',
viewdef: table.name,
},
busrules: '',
class: table.longName,
name: localized(table.name),
view: '',
resourcelabels: 'true',
viewdefs: {
[table.name]: xml,
},
viewsetLevel: '',
viewsetName: '',
viewsetSource: '',
viewsetFile: null,
viewsetId: null,
},
'form',
'edit',
table
);
setViewDefinition(parsed);
} catch {
busrules: '',
class: table.longName,
name: localized(table.name),
view: '',
resourcelabels: 'true',
viewdefs: {
[table.name]: xml,
},
viewsetLevel: '',
viewsetName: '',
viewsetSource: '',
viewsetFile: null,
viewsetId: null,
},
'form',
'edit',
table
)
.then((definition) => setViewDefinition(definition))
// Ignore errors, as they would already be reported by the editor
}
.catch(() => undefined);
}, [xml, table]);
const resource = React.useMemo(() => new table.Resource(), [table]);
const [layout = 'horizontal'] = useCachedState('formEditor', 'layout');
Expand Down
Expand Up @@ -31,6 +31,7 @@ exports[`Can edit a form definition 1`] = `
},
],
},
"businessRules": "edu.ku.brc.specify.datamodel.busrules.AccessionBusRules",
"description": "The Accession form.",
"legacyResourceLabels": false,
"legacyUseBusinessRules": true,
Expand Down
Expand Up @@ -84,6 +84,9 @@ const viewSets = (): ViewSets =>
{
name: localized('CollectionObjectAttachment'),
description: 'The Collection Object-Attachment View.',
businessRules: localized(
'edu.ku.brc.specify.datamodel.busrules.AttachmentBusRules'
),
legacyIsInternal: undefined,
legacyIsExternal: undefined,
legacyTable: undefined,
Expand Down Expand Up @@ -360,6 +363,8 @@ test('Create new view definition', () =>
],
},
description: 'The Collection Object-Attachment View.',
businessRules:
'edu.ku.brc.specify.datamodel.busrules.AttachmentBusRules',
legacyResourceLabels: false,
legacyUseBusinessRules: true,
name: 'CollectionObjectAttachment',
Expand Down Expand Up @@ -414,6 +419,8 @@ test('Create new view definition', () =>
],
},
description: '',
businessRules:
'edu.ku.brc.specify.datamodel.busrules.AttachmentBusRules',
legacyUseBusinessRules: true,
name: 'CollectionObjectAttachment_2',
table: '[table CollectionObjectAttachment]',
Expand Down Expand Up @@ -558,6 +565,8 @@ test('Add new view definition based on existing', () =>
],
},
description: 'The Collection Object-Attachment View.',
businessRules:
'edu.ku.brc.specify.datamodel.busrules.AttachmentBusRules',
legacyResourceLabels: false,
legacyUseBusinessRules: true,
name: 'CollectionObjectAttachment',
Expand Down Expand Up @@ -608,6 +617,8 @@ test('Add new view definition based on existing', () =>
],
},
description: 'The Collection Object-Attachment View.',
businessRules:
'edu.ku.brc.specify.datamodel.busrules.AttachmentBusRules',
legacyResourceLabels: false,
legacyUseBusinessRules: true,
name: 'A',
Expand Down
Expand Up @@ -10,6 +10,7 @@ import { getLogContext, setLogContext } from '../Errors/logContext';
import type { ViewDefinition } from '../FormParse';
import { fromSimpleXmlNode } from '../Syncer/fromSimpleXmlNode';
import { createSimpleXmlNode } from '../Syncer/xmlToJson';
import { getBusinessRuleClassFromTable } from './helpers';
import type { ViewSets } from './spec';
import { parseFormView } from './spec';

Expand Down Expand Up @@ -91,6 +92,7 @@ function createNewView(
title: localized(table.name),
description: '',
table,
businessRules: localized(getBusinessRuleClassFromTable(table.name)),
legacyTable: undefined,
altViews,
legacyIsInternal: undefined,
Expand Down
82 changes: 82 additions & 0 deletions specifyweb/frontend/js_src/lib/components/FormEditor/helpers.ts
@@ -0,0 +1,82 @@
import type { RR } from '../../utils/types';
import type { Tables } from '../DataModel/types';

export const tablesWithBusRulesIn6 = new Set([
'AccessionAgentBusRules',
'AccessionAuthorizationBusRules',
'AccessionBusRules',
'AddressBusRules',
'AgentBusRules',
'AppraisalBusRules',
'AttachmentBusRules',
'AuthorBusRules',
'BioStratBusRules',
'BorrowBusRules',
'CatalogNumberingSchemeBusRules',
'CatAutoNumberingSchemeBusRules',
'CollectingEventBusRules',
'CollectingTripAuthorizationBusRules',
'CollectingTripBusRules',
'CollectionBusRules',
'CollectionObjectBusRules',
'CollectorBusRules',
'ConservDescriptionBusRules',
'ConservEventBusRules',
'ContainerBusRules',
'DataTypeBusRules',
'DeaccessionAgentBusRules',
'DeterminationBusRules',
'DeterminerBusRules',
'DisciplineBusRules',
'DivisionBusRules',
'DNASequenceBusRules',
'DNASequencingRunBusRules',
'ExchangeOutBusRules',
'ExchangeOutPrepBusRules',
'ExtractorBusRules',
'FieldNotebookBusRules',
'FieldNotebookPageSetBusRules',
'FundingAgentBusRules',
'GeographyBusRules',
'GeologicTimePeriodBusRules',
'GiftBusRules',
'GiftPreparationBusRules',
'GroupPersonBusRules',
'InfoRequestBusRules',
'LithoStratBusRules',
'LoanBusRules',
'LoanGiftShipmentBusRules',
'LoanPreparationBusRules',
'LoanReturnPreparationBusRules',
'LocalityBusRules',
'PcrPersonBusRules',
'PermitBusRules',
'PickListBusRules',
'PreparationBusRules',
'PrepTypeBusRules',
'SpecifyUserBusRules',
'StorageBusRules',
'TaxonBusRules',
'TreatmentEventBusRules',
'TreeDefBusRules',
]);

/**
* Most of the time business rules class name can be inferred from table name.
* Exceptions:
*/
export const businessRulesOverride: Partial<RR<keyof Tables, string>> = {
Shipment: 'LoanGiftShipment',
CollectionObjectAttachment: 'Attachment',
};

export const getBusinessRuleClassFromTable = (
tableName: keyof Tables
): string =>
businessRulesOverride[tableName] === undefined
? tablesWithBusRulesIn6.has(`${tableName}BusRules`)
? `edu.ku.brc.specify.datamodel.busrules.${tableName}BusRules`
: ''
: `edu.ku.brc.specify.datamodel.busrules.${businessRulesOverride[
tableName
]!}BusRules`;

0 comments on commit e76e4ff

Please sign in to comment.