From 3971c3169a689ccc0b2879d82408cc06ba4301f9 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Wed, 10 Jul 2024 13:22:27 -0700
Subject: [PATCH 01/78] Add a feature to choose type of taxon tree in WB
Fixes #4980
---
.../lib/components/WbPlanView/Wrapped.tsx | 66 +++++++++++++++----
.../js_src/lib/localization/workbench.ts | 3 +
2 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 4f6e50edefa..8ec8c97747b 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -9,11 +9,26 @@ import { useNavigate } from 'react-router-dom';
import type { LocalizedString } from 'typesafe-i18n';
import type { State } from 'typesafe-reducer';
+import { usePromise } from '../../hooks/useAsyncState';
+import { useCachedState } from '../../hooks/useCachedState';
import { useErrorContext } from '../../hooks/useErrorContext';
import { useLiveState } from '../../hooks/useLiveState';
+import { commonText } from '../../localization/common';
+import { resourcesText } from '../../localization/resources';
+import { treeText } from '../../localization/tree';
+import { wbText } from '../../localization/workbench';
+import { f } from '../../utils/functools';
import type { IR, RA } from '../../utils/types';
+import { caseInsensitiveHash } from '../../utils/utils';
+import { Button } from '../Atoms/Button';
+import { Select } from '../Atoms/Form';
import type { Tables } from '../DataModel/types';
+import {
+ getTreeDefinitions,
+ treeRanksPromise,
+} from '../InitialContext/treeRanks';
import { useTitle } from '../Molecules/AppTitle';
+import { Dialog } from '../Molecules/Dialog';
import { ProtectedAction } from '../Permissions/PermissionDenied';
import type { UploadResult } from '../WorkBench/resultsParser';
import { savePlan } from './helpers';
@@ -121,25 +136,37 @@ export function WbPlanView({
);
useErrorContext('state', state);
+ const [isTaxonTable, setIsTaxonTable] = React.useState(false);
+
+ const [treeDefinitions] = usePromise(treeRanksPromise, true);
+ const definitionsForTree = treeDefinitions
+ ? caseInsensitiveHash(treeDefinitions, 'Taxon')
+ : undefined;
+ const definitions = definitionsForTree?.map(({ definition }) => definition);
+
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
navigate(`/specify/workbench/${dataset.id}/`)}
- onSelected={(baseTableName): void =>
- setState({
- type: 'MappingState',
- changesMade: true,
- baseTableName,
- lines: getLinesFromHeaders({
- headers,
- runAutoMapper: true,
+ onSelected={(baseTableName): void => {
+ if (baseTableName === 'Taxon') {
+ setIsTaxonTable(true);
+ } else {
+ setState({
+ type: 'MappingState',
+ changesMade: true,
baseTableName,
- }),
- mustMatchPreferences: {},
- })
- }
+ lines: getLinesFromHeaders({
+ headers,
+ runAutoMapper: true,
+ baseTableName,
+ }),
+ mustMatchPreferences: {},
+ });
+ }
+ }}
onSelectTemplate={(uploadPlan, headers): void =>
setState({
type: 'MappingState',
@@ -148,6 +175,21 @@ export function WbPlanView({
})
}
/>
+ {isTaxonTable && (
+
+ )}
) : (
Date: Wed, 10 Jul 2024 14:04:17 -0700
Subject: [PATCH 02/78] TODO
---
.../frontend/js_src/lib/components/WbPlanView/navigator.ts | 1 +
.../js_src/lib/components/WbPlanView/uploadPlanBuilder.ts | 1 +
2 files changed, 2 insertions(+)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index 096565a51d1..bde18adf492 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -301,6 +301,7 @@ export function getMappingLineData({
? [[formatToManyIndex(maxMappedElementNumber + 1), commonText.add()]]
: [];
+ // Add prop with tree table name and filter if that prop has been passed
commitInstanceData(
'toMany',
table,
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 1d820221071..a2ac6db5df8 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -38,6 +38,7 @@ const toTreeRecordRanks = (
])
);
+// TODO: need to update this to include tree def identifier
const toTreeRecordVariety = (lines: RA): TreeRecord => ({
ranks: Object.fromEntries(
indexMappings(lines).map(([fullRankName, rankMappedFields]) => [
From c8afd1d2df51d22fde48c4cf96f5b98640b7649e Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Thu, 11 Jul 2024 11:18:10 -0700
Subject: [PATCH 03/78] List of available taxon trees to choose from when
opening WB
---
.../lib/components/WbPlanView/Mapper.tsx | 3 ++
.../lib/components/WbPlanView/Wrapped.tsx | 49 ++++++++++++-------
.../lib/components/WbPlanView/navigator.ts | 9 +++-
3 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index 044dbca8ab3..4abd75a58c9 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -139,6 +139,7 @@ export function Mapper(props: {
readonly changesMade: boolean;
readonly lines: RA;
readonly mustMatchPreferences: IR;
+ readonly taxonType?: string;
}): JSX.Element {
const [state, dispatch] = React.useReducer(
reducer,
@@ -447,6 +448,7 @@ export function Mapper(props: {
mustMatchPreferences: state.mustMatchPreferences,
generateFieldData: 'all',
spec: navigatorSpecs.wbPlanView,
+ taxonType: props.taxonType,
}),
customSelectType: 'OPENED_LIST',
onChange({ isDoubleClick, ...rest }) {
@@ -535,6 +537,7 @@ export function Mapper(props: {
mustMatchPreferences: state.mustMatchPreferences,
generateFieldData: 'all',
spec: navigatorSpecs.wbPlanView,
+ taxonType: props.taxonType,
}),
});
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 8ec8c97747b..d83a50f0c28 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -10,23 +10,16 @@ import type { LocalizedString } from 'typesafe-i18n';
import type { State } from 'typesafe-reducer';
import { usePromise } from '../../hooks/useAsyncState';
-import { useCachedState } from '../../hooks/useCachedState';
import { useErrorContext } from '../../hooks/useErrorContext';
import { useLiveState } from '../../hooks/useLiveState';
import { commonText } from '../../localization/common';
-import { resourcesText } from '../../localization/resources';
-import { treeText } from '../../localization/tree';
import { wbText } from '../../localization/workbench';
-import { f } from '../../utils/functools';
-import type { IR, RA } from '../../utils/types';
+import { type IR, type RA, localized } from '../../utils/types';
import { caseInsensitiveHash } from '../../utils/utils';
+import { Ul } from '../Atoms';
import { Button } from '../Atoms/Button';
-import { Select } from '../Atoms/Form';
import type { Tables } from '../DataModel/types';
-import {
- getTreeDefinitions,
- treeRanksPromise,
-} from '../InitialContext/treeRanks';
+import { treeRanksPromise } from '../InitialContext/treeRanks';
import { useTitle } from '../Molecules/AppTitle';
import { Dialog } from '../Molecules/Dialog';
import { ProtectedAction } from '../Permissions/PermissionDenied';
@@ -139,10 +132,27 @@ export function WbPlanView({
const [isTaxonTable, setIsTaxonTable] = React.useState(false);
const [treeDefinitions] = usePromise(treeRanksPromise, true);
- const definitionsForTree = treeDefinitions
+ const definitionsForTreeTaxon = treeDefinitions
? caseInsensitiveHash(treeDefinitions, 'Taxon')
: undefined;
- const definitions = definitionsForTree?.map(({ definition }) => definition);
+ const definitions = definitionsForTreeTaxon?.map(
+ ({ definition }) => definition
+ );
+ const [taxonType, setTaxonType] = React.useState();
+ const handleTreeType = (taxonTreeDefinitionName: string): void => {
+ setTaxonType(taxonTreeDefinitionName);
+ setState({
+ type: 'MappingState',
+ changesMade: true,
+ baseTableName: 'Taxon',
+ lines: getLinesFromHeaders({
+ headers,
+ runAutoMapper: true,
+ baseTableName: 'Taxon',
+ }),
+ mustMatchPreferences: {},
+ });
+ };
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
@@ -183,11 +193,15 @@ export function WbPlanView({
header={wbText.selectTree()}
onClose={(): void => setIsTaxonTable(false)}
>
- {definitions?.map(({ id, name }) => (
-
- ))}
+
+ {definitions?.map(({ name }) => (
+ -
+ handleTreeType(name)}>
+ {localized(name)}
+
+
+ ))}
+
)}
@@ -198,6 +212,7 @@ export function WbPlanView({
dataset={dataset}
lines={state.lines}
mustMatchPreferences={state.mustMatchPreferences}
+ taxonType={taxonType}
onChangeBaseTable={(): void =>
setState({
type: 'SelectBaseTable',
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index bde18adf492..df92bc94015 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -189,6 +189,7 @@ export function getMappingLineData({
showHiddenFields = false,
mustMatchPreferences = {},
spec,
+ taxonType,
}: {
readonly baseTableName: keyof Tables;
// The mapping path
@@ -203,6 +204,7 @@ export function getMappingLineData({
readonly showHiddenFields?: boolean;
readonly mustMatchPreferences?: IR;
readonly spec: NavigatorSpec;
+ readonly taxonType?: string;
}): RA {
if (
process.env.NODE_ENV !== 'production' &&
@@ -254,7 +256,11 @@ export function getMappingLineData({
customSelectSubtype,
defaultValue: internalState.defaultValue,
selectLabel: table.label,
- fieldsData: Object.fromEntries(filterArray(fieldsData)),
+ fieldsData: Object.fromEntries(
+ filterArray(fieldsData)
+ .filter((field) => field[1].tableTreeDefName === taxonType)
+ .map(([rawKey, fieldData]) => [rawKey, fieldData])
+ ),
tableName: table.name,
});
@@ -301,7 +307,6 @@ export function getMappingLineData({
? [[formatToManyIndex(maxMappedElementNumber + 1), commonText.add()]]
: [];
- // Add prop with tree table name and filter if that prop has been passed
commitInstanceData(
'toMany',
table,
From 8fda5d9485a919d7f6bdecd7d0c473f927525fbb Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Thu, 11 Jul 2024 13:10:04 -0700
Subject: [PATCH 04/78] comments
---
.../js_src/lib/components/WbPlanView/CustomSelectElement.tsx | 1 +
specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx | 1 +
2 files changed, 2 insertions(+)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
index ecab366fb66..f672cb60b52 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
@@ -523,6 +523,7 @@ export function CustomSelectElement({
const defaultOption =
previewOption ??
+ // Where comes from isDefault?
inlineOptions.find(({ isDefault }) => isDefault) ??
defaultDefaultOption;
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index 4abd75a58c9..e5d82990ab3 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -458,6 +458,7 @@ export function Mapper(props: {
dispatch({
type: 'ChangeSelectElementValueAction',
line: 'mappingView',
+ // TableName in rest is undefined and therefore no mapping
...rest,
});
},
From 69cf926e33f1a7ac057a88efa50f3a5b9d01f210 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Thu, 11 Jul 2024 13:52:48 -0700
Subject: [PATCH 05/78] Remove comment
---
.../js_src/lib/components/WbPlanView/CustomSelectElement.tsx | 1 -
specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx | 1 -
2 files changed, 2 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
index f672cb60b52..ecab366fb66 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx
@@ -523,7 +523,6 @@ export function CustomSelectElement({
const defaultOption =
previewOption ??
- // Where comes from isDefault?
inlineOptions.find(({ isDefault }) => isDefault) ??
defaultDefaultOption;
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index e5d82990ab3..4abd75a58c9 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -458,7 +458,6 @@ export function Mapper(props: {
dispatch({
type: 'ChangeSelectElementValueAction',
line: 'mappingView',
- // TableName in rest is undefined and therefore no mapping
...rest,
});
},
From 01c8bd4e275ed80f94b2d01b05297580d6ef5bfd Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Fri, 12 Jul 2024 07:46:33 -0700
Subject: [PATCH 06/78] Don't filter fieldsData if not first call
---
.../frontend/js_src/lib/components/WbPlanView/navigator.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index df92bc94015..1e5bf580948 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -258,7 +258,12 @@ export function getMappingLineData({
selectLabel: table.label,
fieldsData: Object.fromEntries(
filterArray(fieldsData)
- .filter((field) => field[1].tableTreeDefName === taxonType)
+ .filter((field) => {
+ if (internalState.mappingLineData.length === 0) {
+ return field[1].tableTreeDefName === taxonType;
+ }
+ return true;
+ })
.map(([rawKey, fieldData]) => [rawKey, fieldData])
),
tableName: table.name,
From 53f0add4b26b34be013f4e5cf375ff184faeee2f Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Fri, 12 Jul 2024 09:35:46 -0700
Subject: [PATCH 07/78] Pass taxonTreeId with upload plan
---
.../frontend/js_src/lib/components/WbPlanView/Wrapped.tsx | 5 +++++
.../frontend/js_src/lib/components/WbPlanView/helpers.ts | 3 +++
.../frontend/js_src/lib/components/WorkBench/index.tsx | 5 ++++-
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index d83a50f0c28..546003be0ac 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -153,6 +153,10 @@ export function WbPlanView({
mustMatchPreferences: {},
});
};
+ const taxonTree = definitionsForTreeTaxon?.find(
+ (tree) => tree.definition.name === taxonType
+ );
+ const taxonTreeId = taxonTree?.definition.id;
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
@@ -224,6 +228,7 @@ export function WbPlanView({
baseTableName: state.baseTableName,
lines,
mustMatchPreferences,
+ taxonTreeId,
}).then(() => navigate(`/specify/workbench/${dataset.id}/`))
}
/>
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
index 7b7dd213d92..deecea572b1 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
@@ -42,11 +42,13 @@ export async function savePlan({
baseTableName,
lines,
mustMatchPreferences,
+ taxonTreeId,
}: {
readonly dataset: Dataset;
readonly baseTableName: keyof Tables;
readonly lines: RA;
readonly mustMatchPreferences: IR;
+ readonly taxonTreeId?: number | undefined;
}): Promise {
const renamedLines = renameNewlyCreatedHeaders(
baseTableName,
@@ -75,6 +77,7 @@ export async function savePlan({
method: 'PUT',
body: {
uploadplan: uploadPlan,
+ taxonTreeId,
},
}).then(async () =>
newlyAddedHeaders.length === 0
diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
index 5bb210a296f..63cb2684778 100644
--- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
@@ -68,4 +68,7 @@ function useDataset(
const fetchDataset = async (datasetId: number): Promise =>
ajax(`/api/workbench/dataset/${datasetId}/`, {
headers: { Accept: 'application/json' },
- }).then(({ data }) => data);
+ }).then(({ data }) => {
+ console.log('');
+ return data;
+ });
From 3282d1de2ac8c22c6d98476e143f436c2ce82265 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Fri, 12 Jul 2024 09:41:00 -0700
Subject: [PATCH 08/78] Add taxonId to uplaodPlan
---
.../frontend/js_src/lib/components/WbPlanView/helpers.ts | 5 +++--
.../js_src/lib/components/WbPlanView/uploadPlanBuilder.ts | 4 +++-
.../js_src/lib/components/WbPlanView/uploadPlanParser.ts | 1 +
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
index deecea572b1..11adc2b2c65 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
@@ -68,7 +68,8 @@ export async function savePlan({
const uploadPlan = uploadPlanBuilder(
baseTableName,
renamedLines,
- getMustMatchTables({ baseTableName, lines, mustMatchPreferences })
+ getMustMatchTables({ baseTableName, lines, mustMatchPreferences }),
+ taxonTreeId
);
const dataSetRequestUrl = `/api/workbench/dataset/${dataset.id}/`;
@@ -77,7 +78,7 @@ export async function savePlan({
method: 'PUT',
body: {
uploadplan: uploadPlan,
- taxonTreeId,
+ // TaxonTreeId,
},
}).then(async () =>
newlyAddedHeaders.length === 0
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index a2ac6db5df8..ede50bf503d 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -138,7 +138,8 @@ const toUploadable = (
export const uploadPlanBuilder = (
baseTableName: keyof Tables,
lines: RA,
- mustMatchPreferences: RR
+ mustMatchPreferences: RR,
+ taxonTreeId: number | undefined
): UploadPlan => ({
baseTableName: toLowerCase(baseTableName),
uploadable: toUploadable(
@@ -149,6 +150,7 @@ export const uploadPlanBuilder = (
.map(([tableName]) => tableName),
true
),
+ taxonTreeId,
});
const indexMappings = (
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 3a9463ca621..4fc801073c7 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -46,6 +46,7 @@ export type Uploadable = TreeRecordVariety | UploadTableVariety;
export type UploadPlan = {
readonly baseTableName: Lowercase;
readonly uploadable: Uploadable;
+ readonly taxonTreeId: number | undefined;
};
const parseColumnOptions = (matchingOptions: ColumnOptions): ColumnOptions => ({
From 659304fe612705410f3d339a3d47daf0e063aa0f Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Fri, 12 Jul 2024 13:33:38 -0700
Subject: [PATCH 09/78] Use treeDefId to define which tree to look at in mapper
---
.../frontend/js_src/lib/components/WbPlanView/Wrapped.tsx | 7 ++++++-
.../frontend/js_src/lib/components/WorkBench/index.tsx | 5 +----
specifyweb/workbench/views.py | 4 +++-
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 546003be0ac..554a8f68326 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -138,6 +138,7 @@ export function WbPlanView({
const definitions = definitionsForTreeTaxon?.map(
({ definition }) => definition
);
+
const [taxonType, setTaxonType] = React.useState();
const handleTreeType = (taxonTreeDefinitionName: string): void => {
setTaxonType(taxonTreeDefinitionName);
@@ -157,6 +158,10 @@ export function WbPlanView({
(tree) => tree.definition.name === taxonType
);
const taxonTreeId = taxonTree?.definition.id;
+ const taxonCorrespondingId = definitionsForTreeTaxon?.find(
+ (tree) => tree.definition.id === uploadPlan?.taxonTreeId
+ );
+ const taxonIdName = taxonCorrespondingId?.definition.name;
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
@@ -216,7 +221,7 @@ export function WbPlanView({
dataset={dataset}
lines={state.lines}
mustMatchPreferences={state.mustMatchPreferences}
- taxonType={taxonType}
+ taxonType={taxonType ?? taxonIdName}
onChangeBaseTable={(): void =>
setState({
type: 'SelectBaseTable',
diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
index 63cb2684778..5bb210a296f 100644
--- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx
@@ -68,7 +68,4 @@ function useDataset(
const fetchDataset = async (datasetId: number): Promise =>
ajax(`/api/workbench/dataset/${datasetId}/`, {
headers: { Accept: 'application/json' },
- }).then(({ data }) => {
- console.log('');
- return data;
- });
+ }).then(({ data }) => data);
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index ead475776e4..b77dc83a1ee 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -477,7 +477,9 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
try:
validate(plan, upload_plan_schema.schema)
except ValidationError as e:
- return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
+ # TODO fix this
+ # return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
+ pass
new_cols = upload_plan_schema.parse_plan(request.specify_collection, plan).get_cols() - set(ds.columns)
if new_cols:
From eeac2333479bc7b69c0dc1b63ad66ec8c68d7bb7 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Tue, 16 Jul 2024 09:06:42 -0700
Subject: [PATCH 10/78] Remove comment
---
specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
index 11adc2b2c65..af69d9667e7 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
@@ -78,7 +78,6 @@ export async function savePlan({
method: 'PUT',
body: {
uploadplan: uploadPlan,
- // TaxonTreeId,
},
}).then(async () =>
newlyAddedHeaders.length === 0
From dd675ba98a8f39d08bd040fa2ece30f04d494632 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Tue, 16 Jul 2024 09:47:35 -0700
Subject: [PATCH 11/78] Add treeId to upload plan
---
specifyweb/workbench/upload/upload_plan_schema.py | 13 +++++++++++++
specifyweb/workbench/views.py | 4 +---
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 3826fbf4263..e617036d962 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -34,6 +34,12 @@
},
]
},
+ "taxonTreeId": {
+ "oneOf": [
+ { "type": "integer" },
+ { "type": "null" }
+ ]
+ }
},
'required': [ 'baseTableName', 'uploadable' ],
'additionalProperties': False,
@@ -140,6 +146,13 @@
],
},
+ 'taxonTreeId': {
+ "oneOf": [
+ { "type": "integer" },
+ { "type": "null" }
+ ]
+ },
+
'wbcols': {
'type': 'object',
'description': 'Maps the columns of the destination table to the headers of the source columns of input data.',
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index b77dc83a1ee..ead475776e4 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -477,9 +477,7 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
try:
validate(plan, upload_plan_schema.schema)
except ValidationError as e:
- # TODO fix this
- # return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
- pass
+ return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
new_cols = upload_plan_schema.parse_plan(request.specify_collection, plan).get_cols() - set(ds.columns)
if new_cols:
From 670fc872ffe8c1d9306a7db5cb6f5d7d046bdf1d Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Tue, 16 Jul 2024 10:44:51 -0700
Subject: [PATCH 12/78] Fix filtering when taxonType is undefined, needed in QB
---
.../frontend/js_src/lib/components/WbPlanView/navigator.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index 1e5bf580948..f67123204d5 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -259,7 +259,10 @@ export function getMappingLineData({
fieldsData: Object.fromEntries(
filterArray(fieldsData)
.filter((field) => {
- if (internalState.mappingLineData.length === 0) {
+ if (
+ internalState.mappingLineData.length === 0 &&
+ taxonType !== undefined
+ ) {
return field[1].tableTreeDefName === taxonType;
}
return true;
From e7e83aeebb06f69f0faf34cff52e7e14b576bdbc Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Fri, 2 Aug 2024 17:37:37 -0500
Subject: [PATCH 13/78] Start work on adding new column in WB for multiple
trees
---
.../lib/components/WbPlanView/Mapper.tsx | 9 ++++
.../lib/components/WbPlanView/Wrapped.tsx | 15 ++++---
.../lib/components/WbPlanView/navigator.ts | 42 +++++++++++++++----
3 files changed, 53 insertions(+), 13 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index 4abd75a58c9..ef6d2a874e9 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -24,10 +24,15 @@ import { className } from '../Atoms/className';
import { icons } from '../Atoms/Icons';
import { Link } from '../Atoms/Link';
import { LoadingContext, ReadOnlyContext } from '../Core/Contexts';
+import type {
+ FilterTablesByEndsWith,
+ SerializedResource,
+} from '../DataModel/helperTypes';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { softFail } from '../Errors/Crash';
import { ErrorBoundary } from '../Errors/ErrorBoundary';
+import type { TreeInformation } from '../InitialContext/treeRanks';
import { TableIcon } from '../Molecules/TableIcon';
import { Layout } from './Header';
import {
@@ -140,6 +145,9 @@ export function Mapper(props: {
readonly lines: RA;
readonly mustMatchPreferences: IR;
readonly taxonType?: string;
+ readonly treeDefinitions:
+ | readonly SerializedResource>[]
+ | undefined;
}): JSX.Element {
const [state, dispatch] = React.useReducer(
reducer,
@@ -449,6 +457,7 @@ export function Mapper(props: {
generateFieldData: 'all',
spec: navigatorSpecs.wbPlanView,
taxonType: props.taxonType,
+ treeDefinitions: props.treeDefinitions,
}),
customSelectType: 'OPENED_LIST',
onChange({ isDoubleClick, ...rest }) {
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 554a8f68326..2821282505f 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -169,10 +169,13 @@ export function WbPlanView({
navigate(`/specify/workbench/${dataset.id}/`)}
- onSelected={(baseTableName): void => {
- if (baseTableName === 'Taxon') {
- setIsTaxonTable(true);
- } else {
+ onSelected={
+ (baseTableName): void => {
+ /*
+ * If (baseTableName === 'Taxon') {
+ * setIsTaxonTable(true);
+ * } else {
+ */
setState({
type: 'MappingState',
changesMade: true,
@@ -185,7 +188,8 @@ export function WbPlanView({
mustMatchPreferences: {},
});
}
- }}
+ // }
+ }
onSelectTemplate={(uploadPlan, headers): void =>
setState({
type: 'MappingState',
@@ -222,6 +226,7 @@ export function WbPlanView({
lines={state.lines}
mustMatchPreferences={state.mustMatchPreferences}
taxonType={taxonType ?? taxonIdName}
+ treeDefinitions={definitions}
onChangeBaseTable={(): void =>
setState({
type: 'SelectBaseTable',
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index 8911ed58e67..e3cba7a6676 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -10,11 +10,16 @@ import { queryText } from '../../localization/query';
import type { IR, RA, WritableArray } from '../../utils/types';
import { defined, filterArray } from '../../utils/types';
import { dateParts } from '../Atoms/Internationalization';
-import type { AnyTree } from '../DataModel/helperTypes';
+import type {
+ AnyTree,
+ FilterTablesByEndsWith,
+ SerializedResource,
+} from '../DataModel/helperTypes';
import type { Relationship } from '../DataModel/specifyField';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { getFrontEndOnlyFields, strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
+import type { TreeInformation } from '../InitialContext/treeRanks';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { hasTablePermission, hasTreeAccess } from '../Permissions/helpers';
import type { CustomSelectSubtype } from './CustomSelectElement';
@@ -195,6 +200,7 @@ export function getMappingLineData({
mustMatchPreferences = {},
spec,
taxonType,
+ treeDefinitions,
}: {
readonly baseTableName: keyof Tables;
// The mapping path
@@ -210,6 +216,9 @@ export function getMappingLineData({
readonly mustMatchPreferences?: IR;
readonly spec: NavigatorSpec;
readonly taxonType?: string;
+ readonly treeDefinitions:
+ | readonly SerializedResource>[]
+ | undefined;
}): RA {
if (
process.env.NODE_ENV !== 'production' &&
@@ -264,7 +273,9 @@ export function getMappingLineData({
fieldsData: Object.fromEntries(
filterArray(fieldsData)
.filter((field) => {
- if (
+ if (table.name === 'Taxon' && mappingPath[0] === '0') {
+ return treeDefinitions?.map((definition) => definition.name);
+ } else if (
internalState.mappingLineData.length === 0 &&
taxonType !== undefined
) {
@@ -350,12 +361,24 @@ export function getMappingLineData({
handleTreeRanks({ table }) {
const defaultValue = getNameFromTreeRankName(internalState.defaultValue);
- commitInstanceData(
- 'tree',
- table,
+ const fieldsData: readonly (
+ | readonly [string, HtmlGeneratorFieldData]
+ | undefined
+ )[] =
generateFieldData === 'none' ||
- !hasTreeAccess(table.name as 'Geography', 'read')
+ !hasTreeAccess(table.name as 'Geography', 'read')
? []
+ : table.name === 'Taxon' && mappingPath[0] === '0' && treeDefinitions
+ ? treeDefinitions.map((treeDefinition) => [
+ treeDefinition.name,
+ {
+ optionLabel: treeDefinition.name,
+ isRelationship: true,
+ isDefault: false,
+ tableName: 'Taxon',
+ tableTreeDefName: treeDefinition.name,
+ },
+ ])
: [
spec.includeAnyTreeRank &&
(generateFieldData === 'all' ||
@@ -395,8 +418,11 @@ export function getMappingLineData({
)
)
: []),
- ]
- );
+ ];
+
+ console.log(fieldsData);
+
+ commitInstanceData('tree', table, fieldsData);
},
handleSimpleFields({ table, parentRelationship }) {
From f8dec7fbecf75d4786426a5990c94dfd0f2e886b Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Mon, 5 Aug 2024 09:11:36 -0700
Subject: [PATCH 14/78] add handleTreeSelection
---
.../js_src/lib/components/WbPlanView/Mapper.tsx | 3 +++
.../js_src/lib/components/WbPlanView/Wrapped.tsx | 13 ++++++++++++-
.../js_src/lib/components/WbPlanView/navigator.ts | 13 ++++++++-----
3 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index ef6d2a874e9..3125f322e3b 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -148,6 +148,7 @@ export function Mapper(props: {
readonly treeDefinitions:
| readonly SerializedResource>[]
| undefined;
+ readonly handleTreeSelection: (treeName: string) => void;
}): JSX.Element {
const [state, dispatch] = React.useReducer(
reducer,
@@ -461,6 +462,8 @@ export function Mapper(props: {
}),
customSelectType: 'OPENED_LIST',
onChange({ isDoubleClick, ...rest }) {
+ if (rest.index === 0 && rest.newTableName === 'Taxon')
+ props.handleTreeSelection(rest.newValue);
if (isDoubleClick && mapButtonEnabled)
dispatch({ type: 'MappingViewMapAction' });
else if (!isReadOnly)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 2821282505f..5ffccd52b74 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -163,6 +163,16 @@ export function WbPlanView({
);
const taxonIdName = taxonCorrespondingId?.definition.name;
+ const [treeSelected, setTreeSelected] = React.useState();
+
+ const handleTreeSelection = (treeName: string): void => {
+ const taxonCorresponding = definitionsForTreeTaxon?.find(
+ (tree) => tree.definition.name === treeName
+ );
+ const taxonIdName = taxonCorresponding?.definition.name;
+ setTreeSelected(taxonIdName);
+ };
+
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
@@ -223,9 +233,10 @@ export function WbPlanView({
baseTableName={state.baseTableName}
changesMade={state.changesMade}
dataset={dataset}
+ handleTreeSelection={handleTreeSelection}
lines={state.lines}
mustMatchPreferences={state.mustMatchPreferences}
- taxonType={taxonType ?? taxonIdName}
+ taxonType={taxonType ?? treeSelected}
treeDefinitions={definitions}
onChangeBaseTable={(): void =>
setState({
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index e3cba7a6676..31a02113a02 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -275,12 +275,15 @@ export function getMappingLineData({
.filter((field) => {
if (table.name === 'Taxon' && mappingPath[0] === '0') {
return treeDefinitions?.map((definition) => definition.name);
- } else if (
- internalState.mappingLineData.length === 0 &&
- taxonType !== undefined
- ) {
- return field[1].tableTreeDefName === taxonType;
}
+ /*
+ * Else if (
+ * internalState.mappingLineData.length === 0 &&
+ * taxonType !== undefined
+ * ) {
+ * return field[1].tableTreeDefName === taxonType;
+ * }
+ */
return true;
})
.map(([rawKey, fieldData]) => [rawKey, fieldData])
From 22ccb87a3ee93620b434d9d8bba4902d29c60734 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:45:20 -0700
Subject: [PATCH 15/78] Chnage variable name (duplicate)
---
.../frontend/js_src/lib/components/WbPlanView/navigator.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index 31a02113a02..341329c0dab 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -364,7 +364,7 @@ export function getMappingLineData({
handleTreeRanks({ table }) {
const defaultValue = getNameFromTreeRankName(internalState.defaultValue);
- const fieldsData: readonly (
+ const fieldsTreeData: readonly (
| readonly [string, HtmlGeneratorFieldData]
| undefined
)[] =
@@ -423,9 +423,7 @@ export function getMappingLineData({
: []),
];
- console.log(fieldsData);
-
- commitInstanceData('tree', table, fieldsData);
+ commitInstanceData('tree', table, fieldsTreeData);
},
handleSimpleFields({ table, parentRelationship }) {
From fb477d0d46684d2e31c3151d3c584dcff1272c99 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 09:47:23 -0500
Subject: [PATCH 16/78] init wb uploads handling taxon ranks with treedefid
---
specifyweb/workbench/upload/scoping.py | 3 +-
specifyweb/workbench/upload/treerecord.py | 52 ++++++++++--
.../workbench/upload/upload_plan_schema.py | 83 +++++++++++++++++--
specifyweb/workbench/views.py | 10 ++-
4 files changed, 132 insertions(+), 16 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 2d84a98a8ba..07e5213cc12 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -188,8 +188,7 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
if table.name == 'Taxon':
# TODO: Add scoping in the workbench via the Upload Plan
- # treedef = get_taxon_treedef(collection)
- treedef = collection.discipline.taxontreedef
+ treedef = models.Taxontreedef.objects.filter(id=tr.treedef_id).first()
elif table.name == 'Geography':
treedef = collection.discipline.geographytreedef
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index d34f7619aa5..5f7a71b4c2f 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -23,25 +23,48 @@
class TreeRecord(NamedTuple):
name: str
ranks: Dict[str, Dict[str, ColumnOptions]]
+ # treedef_id: int
def apply_scoping(self, collection) -> "ScopedTreeRecord":
from .scoping import apply_scoping_to_treerecord as apply_scoping
return apply_scoping(self, collection)
def get_cols(self) -> Set[str]:
- return set(col.column for r in self.ranks.values() for col in r.values())
+ return {col.column for r in self.ranks.values() for col in r.values() if hasattr(col, 'column')}
def to_json(self) -> Dict:
result = {
- 'ranks': {
- rank: cols['name'].to_json() if len(cols) == 1 else dict(treeNodeCols={k: v.to_json() for k, v in cols.items()})
+ "ranks": {
+ rank: (
+ cols["name"].to_json()
+ if len(cols) == 1
+ else dict(
+ treeNodeCols={
+ k: v.to_json() if hasattr(v, "to_json") else v
+ for k, v in cols.items()
+ }
+ )
+ )
for rank, cols in self.ranks.items()
},
}
- return { 'treeRecord': result }
+ return {'treeRecord': result}
+
+ # def to_json(self) -> Dict:
+ # result = {'ranks': {}}
+ # for rank, cols in self.ranks.items():
+ # if len(cols) == 1:
+ # result['ranks'][rank] = cols['name'].to_json()
+ # else:
+ # treeNodeCols = {}
+ # for k, v in cols.items():
+ # treeNodeCols[k] = v.to_json() if hasattr(v, 'to_json') else v
+ # result['ranks'][rank] = {'treeNodeCols': treeNodeCols}
+ # return {'treeRecord': result}
def unparse(self) -> Dict:
- return { 'baseTableName': self.name, 'uploadble': self.to_json() }
+ return { 'baseTableName': self.name,
+ 'uploadble': self.to_json() }
class ScopedTreeRecord(NamedTuple):
name: str
@@ -50,6 +73,7 @@ class ScopedTreeRecord(NamedTuple):
treedefitems: List
root: Optional[Any]
disambiguation: Dict[str, int]
+ ranks_treedefid_map: Dict[str, int]
def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
return self._replace(disambiguation=disambiguation.disambiguate_tree()) if disambiguation is not None else self
@@ -87,6 +111,24 @@ def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: A
auditor=auditor,
cache=cache,
)
+
+ def get_cols(self) -> Set[str]:
+ return set(col.column for r in self.ranks.values() for col in r.values())
+
+ # def apply_scoping(self, collection) -> "ScopedTreeRecord":
+ # return self
+
+ # def to_json(self) -> Dict:
+ # result = {
+ # 'ranks': {
+ # rank: cols['name'].to_json() if len(cols) == 1 else dict(treeNodeCols={k: v.to_json() for k, v in cols.items()})
+ # for rank, cols in self.ranks.items()
+ # },
+ # }
+ # return { 'treeRecord': result }
+
+ # def unparse(self) -> Dict:
+ # return { 'baseTableName': self.name, 'uploadble': self.to_json() }
class MustMatchTreeRecord(TreeRecord):
def apply_scoping(self, collection) -> "ScopedMustMatchTreeRecord":
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index e617036d962..8cd9c1fdb7c 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -1,16 +1,19 @@
-from typing import Dict, Any, Union, Tuple
+from os import name
+from typing import Dict, Any, Optional, Union, Tuple
+import logging
from specifyweb.specify.datamodel import datamodel, Table, Relationship
from specifyweb.specify.load_datamodel import DoesNotExistError
-from specifyweb.specify import models
+from specifyweb.specify.models import Taxontreedefitem, Taxontreedef
from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable
from .tomany import ToManyRecord
-from .treerecord import TreeRecord, MustMatchTreeRecord
+from .treerecord import TreeRecord, ScopedTreeRecord, MustMatchTreeRecord
from .uploadable import Uploadable
from .column_options import ColumnOptions
from .scoping import DEFERRED_SCOPING
+logger = logging.getLogger(__name__)
schema: Dict = {
'title': 'Specify 7 Workbench Upload Plan',
@@ -228,14 +231,14 @@
}
-def parse_plan_with_basetable(collection, to_parse: Dict) -> Tuple[Table, Uploadable]:
+def parse_plan_with_basetable(collection, to_parse: Dict, extra: Dict = {}) -> Tuple[Table, Uploadable]:
base_table = datamodel.get_table_strict(to_parse['baseTableName'])
return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'])
-def parse_plan(collection, to_parse: Dict) -> Uploadable:
- return parse_plan_with_basetable(collection, to_parse)[1]
+def parse_plan(collection, to_parse: Dict, extra: Dict = {}) -> Uploadable:
+ return parse_plan_with_basetable(collection, to_parse, extra)[1]
-def parse_uploadable(collection, table: Table, to_parse: Dict) -> Uploadable:
+def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> Uploadable:
if 'uploadTable' in to_parse:
return parse_upload_table(collection, table, to_parse['uploadTable'])
@@ -249,7 +252,10 @@ def parse_uploadable(collection, table: Table, to_parse: Dict) -> Uploadable:
return MustMatchTreeRecord(*parse_tree_record(collection, table, to_parse['mustMatchTreeRecord']))
if 'treeRecord' in to_parse:
- return parse_tree_record(collection, table, to_parse['treeRecord'])
+ treedefid = None
+ if 'treedefid' in to_parse.keys():
+ treedefid = int(to_parse['treedefid'])
+ return parse_tree_record(collection, table, to_parse['treeRecord'], treedefid)
raise ValueError('unknown uploadable type')
@@ -314,6 +320,67 @@ def parse_tree_record(collection, table: Table, to_parse: Dict) -> TreeRecord:
ranks=ranks,
)
+def parse_tree_record_2(collection, table: Table, to_parse: Dict, base_treedef_id: Optional[int] = None) -> TreeRecord:
+ ranks = {}
+ ranks_treedefid_map = {}
+ treedefitems = []
+ for rank, name_or_cols in to_parse['ranks'].items():
+ if isinstance(name_or_cols, str):
+ ranks[rank] = {'name': parse_column_options(name_or_cols)}
+ else:
+ ranks[rank] = {}
+ for k, v in name_or_cols['treeNodeCols'].items():
+ ranks[rank][k] = parse_column_options(v)
+ if 'treedefid' in name_or_cols.keys():
+ treedef_id = int(name_or_cols['treedefid'])
+ ranks_treedefid_map[rank] = treedef_id
+ treedefitems.append(Taxontreedefitem.objects.filter(treedef_id=treedef_id, name=rank).first())
+ ranks[rank]['treedefid'] = treedef_id
+ elif 'treedefname' in name_or_cols.keys():
+ treedef_id = Taxontreedef.objects.filter(name=name_or_cols['treedefname']).first().id
+ ranks[rank]['treedefid'] = str(treedef_id)
+ else:
+ taxontreedefitems = Taxontreedefitem.objects.filter(name=rank)
+ # if taxontreedefitems.count() > 1:
+ # raise ValueError(f"Multiple treedefitems with name {rank}")
+ treedefitem = taxontreedefitems.first()
+ # if treedefitem is None:
+ # raise ValueError(f"Could not find treedefitem with name {rank}")
+ treedef_id = treedefitem.treedef_id
+ ranks[rank]['treedefid'] = str(treedef_id)
+ # TODO: Handle cases where two ranks have the same name but different treedefids
+
+ for rank, cols in ranks.items():
+ assert 'name' in cols, to_parse
+
+ return TreeRecord(
+ name=table.django_name,
+ ranks=ranks
+ )
+
+def adjust_upload_plan(plan: Dict) -> Dict:
+ def get_treedef_id(rank_name: str, base_treedef_id: Optional[int]) -> int:
+ if base_treedef_id is not None:
+ treedef = Taxontreedefitem.objects.filter(treedef_id=base_treedef_id, name=rank_name).first()
+ if treedef:
+ if treedef.count() > 1:
+ raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {base_treedef_id}")
+ return base_treedef_id
+ treedef = Taxontreedefitem.objects.filter(name=rank_name).first()
+ if treedef:
+ return treedef.id
+ raise DoesNotExistError(f"Could not find treedefitem with name {rank_name}")
+
+ if plan['baseTableName'] == 'taxon':
+ base_treedef_id = plan.get('taxonTreeId', None)
+ for rank, name_or_cols in plan['uploadable']['treeRecord']['ranks'].items():
+ if isinstance(name_or_cols, dict):
+ rank_name = name_or_cols['treeNodeCols']['name']
+ if 'treedefid' not in name_or_cols:
+ name_or_cols['treedefid'] = get_treedef_id(rank_name, base_treedef_id)
+
+ return plan
+
def parse_to_many_record(collection, table: Table, to_parse: Dict) -> ToManyRecord:
def rel_table(key: str) -> Table:
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index b77dc83a1ee..f3a271bd59a 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -481,7 +481,15 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
# return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
pass
- new_cols = upload_plan_schema.parse_plan(request.specify_collection, plan).get_cols() - set(ds.columns)
+ extra = {}
+ if 'taxonTreeId' in plan.keys():
+ extra['treedefid'] = plan['taxonTreeId']
+
+ parsed_plan = upload_plan_schema.parse_plan(request.specify_collection, plan, extra)
+ new_cols = parsed_plan.get_cols() - set(ds.columns)
+ if plan['baseTableName'] == 'taxon':
+ # plan['uploadable'] = parsed_plan.to_json()
+ plan = upload_plan_schema.adjust_upload_plan(plan)
if new_cols:
ncols = len(ds.columns)
ds.columns += list(new_cols)
From 4b12da588b86969ce768e0fa03bb7305389be11f Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 11:07:44 -0500
Subject: [PATCH 17/78] simplify wb mutli-tree solution
---
specifyweb/workbench/upload/scoping.py | 11 ++-
specifyweb/workbench/upload/treerecord.py | 33 +------
.../workbench/upload/upload_plan_schema.py | 94 ++++++++-----------
specifyweb/workbench/views.py | 7 +-
4 files changed, 47 insertions(+), 98 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 07e5213cc12..7815fba9489 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -187,8 +187,10 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
table = datamodel.get_table_strict(tr.name)
if table.name == 'Taxon':
- # TODO: Add scoping in the workbench via the Upload Plan
- treedef = models.Taxontreedef.objects.filter(id=tr.treedef_id).first()
+ if tr.treedef_id is not None:
+ treedef = models.Taxontreedef.objects.filter(id=tr.treedef_id).first()
+ else:
+ treedef = collection.discipline.taxontreedef
elif table.name == 'Geography':
treedef = collection.discipline.geographytreedef
@@ -205,6 +207,9 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
else:
raise Exception(f'unexpected tree type: {table.name}')
+ if treedef is None:
+ raise ValueError(f"Could not find treedef for table {table.name}")
+
treedefitems = list(treedef.treedefitems.order_by('rankid'))
treedef_ranks = [tdi.name for tdi in treedefitems]
for rank in tr.ranks:
@@ -217,7 +222,7 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
name=tr.name,
ranks={r: {f: extend_columnoptions(colopts, collection, table.name, f) for f, colopts in cols.items()} for r, cols in tr.ranks.items()},
treedef=treedef,
- treedefitems=list(treedef.treedefitems.order_by('rankid')),
+ treedefitems=treedefitems,
root=root[0] if root else None,
disambiguation={},
)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 5f7a71b4c2f..27493fc9d27 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -23,7 +23,7 @@
class TreeRecord(NamedTuple):
name: str
ranks: Dict[str, Dict[str, ColumnOptions]]
- # treedef_id: int
+ treedef_id: Optional[int] = None
def apply_scoping(self, collection) -> "ScopedTreeRecord":
from .scoping import apply_scoping_to_treerecord as apply_scoping
@@ -50,18 +50,6 @@ def to_json(self) -> Dict:
}
return {'treeRecord': result}
- # def to_json(self) -> Dict:
- # result = {'ranks': {}}
- # for rank, cols in self.ranks.items():
- # if len(cols) == 1:
- # result['ranks'][rank] = cols['name'].to_json()
- # else:
- # treeNodeCols = {}
- # for k, v in cols.items():
- # treeNodeCols[k] = v.to_json() if hasattr(v, 'to_json') else v
- # result['ranks'][rank] = {'treeNodeCols': treeNodeCols}
- # return {'treeRecord': result}
-
def unparse(self) -> Dict:
return { 'baseTableName': self.name,
'uploadble': self.to_json() }
@@ -73,7 +61,6 @@ class ScopedTreeRecord(NamedTuple):
treedefitems: List
root: Optional[Any]
disambiguation: Dict[str, int]
- ranks_treedefid_map: Dict[str, int]
def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
return self._replace(disambiguation=disambiguation.disambiguate_tree()) if disambiguation is not None else self
@@ -111,24 +98,6 @@ def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: A
auditor=auditor,
cache=cache,
)
-
- def get_cols(self) -> Set[str]:
- return set(col.column for r in self.ranks.values() for col in r.values())
-
- # def apply_scoping(self, collection) -> "ScopedTreeRecord":
- # return self
-
- # def to_json(self) -> Dict:
- # result = {
- # 'ranks': {
- # rank: cols['name'].to_json() if len(cols) == 1 else dict(treeNodeCols={k: v.to_json() for k, v in cols.items()})
- # for rank, cols in self.ranks.items()
- # },
- # }
- # return { 'treeRecord': result }
-
- # def unparse(self) -> Dict:
- # return { 'baseTableName': self.name, 'uploadble': self.to_json() }
class MustMatchTreeRecord(TreeRecord):
def apply_scoping(self, collection) -> "ScopedMustMatchTreeRecord":
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 8cd9c1fdb7c..35dafdf7954 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -8,7 +8,7 @@
from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable
from .tomany import ToManyRecord
-from .treerecord import TreeRecord, ScopedTreeRecord, MustMatchTreeRecord
+from .treerecord import TreeRecord, MustMatchTreeRecord
from .uploadable import Uploadable
from .column_options import ColumnOptions
from .scoping import DEFERRED_SCOPING
@@ -231,12 +231,15 @@
}
-def parse_plan_with_basetable(collection, to_parse: Dict, extra: Dict = {}) -> Tuple[Table, Uploadable]:
+def parse_plan_with_basetable(collection, to_parse: Dict) -> Tuple[Table, Uploadable]:
base_table = datamodel.get_table_strict(to_parse['baseTableName'])
- return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'])
+ extra = {}
+ if 'taxonTreeId' in to_parse.keys():
+ extra['treedefid'] = to_parse['taxonTreeId']
+ return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'], extra)
-def parse_plan(collection, to_parse: Dict, extra: Dict = {}) -> Uploadable:
- return parse_plan_with_basetable(collection, to_parse, extra)[1]
+def parse_plan(collection, to_parse: Dict) -> Uploadable:
+ return parse_plan_with_basetable(collection, to_parse)[1]
def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> Uploadable:
if 'uploadTable' in to_parse:
@@ -253,8 +256,8 @@ def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {})
if 'treeRecord' in to_parse:
treedefid = None
- if 'treedefid' in to_parse.keys():
- treedefid = int(to_parse['treedefid'])
+ if 'treedefid' in extra.keys():
+ treedefid = int(extra['treedefid'])
return parse_tree_record(collection, table, to_parse['treeRecord'], treedefid)
raise ValueError('unknown uploadable type')
@@ -306,7 +309,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
}
)
-def parse_tree_record(collection, table: Table, to_parse: Dict) -> TreeRecord:
+def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
ranks = {
rank: {'name': parse_column_options(name_or_cols)} if isinstance(name_or_cols, str)
else {k: parse_column_options(v) for k,v in name_or_cols['treeNodeCols'].items() }
@@ -315,69 +318,46 @@ def parse_tree_record(collection, table: Table, to_parse: Dict) -> TreeRecord:
for rank, cols in ranks.items():
assert 'name' in cols, to_parse
+ if base_treedefid is None:
+ for rank, cols in ranks.items():
+ treedefid = get_treedef_id(cols['name'].column, False, base_treedefid)
+ if treedefid is not None:
+ base_treedefid = treedefid
+ break
+
return TreeRecord(
name=table.django_name,
ranks=ranks,
+ treedef_id=base_treedefid
)
+
+def get_treedef_id(rank_name: str, is_adjusting: bool = False, base_treedef_id: Optional[int] = None) -> Optional[int]:
+ def find_treedef(treedef_id: Optional[int] = None):
+ filter_kwargs = {'name': rank_name}
+ if treedef_id is not None:
+ filter_kwargs['treedef_id'] = str(treedef_id) # or treedef_id ?
+ return Taxontreedefitem.objects.filter(**filter_kwargs).first()
-def parse_tree_record_2(collection, table: Table, to_parse: Dict, base_treedef_id: Optional[int] = None) -> TreeRecord:
- ranks = {}
- ranks_treedefid_map = {}
- treedefitems = []
- for rank, name_or_cols in to_parse['ranks'].items():
- if isinstance(name_or_cols, str):
- ranks[rank] = {'name': parse_column_options(name_or_cols)}
- else:
- ranks[rank] = {}
- for k, v in name_or_cols['treeNodeCols'].items():
- ranks[rank][k] = parse_column_options(v)
- if 'treedefid' in name_or_cols.keys():
- treedef_id = int(name_or_cols['treedefid'])
- ranks_treedefid_map[rank] = treedef_id
- treedefitems.append(Taxontreedefitem.objects.filter(treedef_id=treedef_id, name=rank).first())
- ranks[rank]['treedefid'] = treedef_id
- elif 'treedefname' in name_or_cols.keys():
- treedef_id = Taxontreedef.objects.filter(name=name_or_cols['treedefname']).first().id
- ranks[rank]['treedefid'] = str(treedef_id)
- else:
- taxontreedefitems = Taxontreedefitem.objects.filter(name=rank)
- # if taxontreedefitems.count() > 1:
- # raise ValueError(f"Multiple treedefitems with name {rank}")
- treedefitem = taxontreedefitems.first()
- # if treedefitem is None:
- # raise ValueError(f"Could not find treedefitem with name {rank}")
- treedef_id = treedefitem.treedef_id
- ranks[rank]['treedefid'] = str(treedef_id)
- # TODO: Handle cases where two ranks have the same name but different treedefids
+ treedef = find_treedef(base_treedef_id)
+ if treedef:
+ return treedef.id
- for rank, cols in ranks.items():
- assert 'name' in cols, to_parse
+ treedef = find_treedef()
+ if treedef:
+ return treedef.id
- return TreeRecord(
- name=table.django_name,
- ranks=ranks
- )
-
-def adjust_upload_plan(plan: Dict) -> Dict:
- def get_treedef_id(rank_name: str, base_treedef_id: Optional[int]) -> int:
- if base_treedef_id is not None:
- treedef = Taxontreedefitem.objects.filter(treedef_id=base_treedef_id, name=rank_name).first()
- if treedef:
- if treedef.count() > 1:
- raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {base_treedef_id}")
- return base_treedef_id
- treedef = Taxontreedefitem.objects.filter(name=rank_name).first()
- if treedef:
- return treedef.id
- raise DoesNotExistError(f"Could not find treedefitem with name {rank_name}")
+ if is_adjusting:
+ raise ValueError(f"Could not find treedefitem with name {rank_name}")
+ return None
+def adjust_upload_plan(plan: Dict) -> Dict:
if plan['baseTableName'] == 'taxon':
base_treedef_id = plan.get('taxonTreeId', None)
for rank, name_or_cols in plan['uploadable']['treeRecord']['ranks'].items():
if isinstance(name_or_cols, dict):
rank_name = name_or_cols['treeNodeCols']['name']
if 'treedefid' not in name_or_cols:
- name_or_cols['treedefid'] = get_treedef_id(rank_name, base_treedef_id)
+ name_or_cols['treedefid'] = str(get_treedef_id(rank_name, True, base_treedef_id))
return plan
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index f3a271bd59a..70bb7270c9c 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -480,15 +480,10 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
# TODO fix this
# return http.HttpResponse(f"upload plan is invalid: {e}", status=400)
pass
-
- extra = {}
- if 'taxonTreeId' in plan.keys():
- extra['treedefid'] = plan['taxonTreeId']
- parsed_plan = upload_plan_schema.parse_plan(request.specify_collection, plan, extra)
+ parsed_plan = upload_plan_schema.parse_plan(request.specify_collection, plan)
new_cols = parsed_plan.get_cols() - set(ds.columns)
if plan['baseTableName'] == 'taxon':
- # plan['uploadable'] = parsed_plan.to_json()
plan = upload_plan_schema.adjust_upload_plan(plan)
if new_cols:
ncols = len(ds.columns)
From 57f9a09f7d538173e366b004c92c7368866639fb Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 12:19:09 -0500
Subject: [PATCH 18/78] fix taxon treedef scoping logic
---
specifyweb/workbench/upload/scoping.py | 3 ++-
specifyweb/workbench/upload/upload_plan_schema.py | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 7815fba9489..a36b65f47ae 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -186,10 +186,11 @@ def apply_scoping_to_tomanyrecord(tmr: ToManyRecord, collection) -> ScopedToMany
def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
table = datamodel.get_table_strict(tr.name)
+ treedef = None
if table.name == 'Taxon':
if tr.treedef_id is not None:
treedef = models.Taxontreedef.objects.filter(id=tr.treedef_id).first()
- else:
+ if treedef is None:
treedef = collection.discipline.taxontreedef
elif table.name == 'Geography':
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 35dafdf7954..63a31f0127a 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -331,7 +331,7 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
treedef_id=base_treedefid
)
-def get_treedef_id(rank_name: str, is_adjusting: bool = False, base_treedef_id: Optional[int] = None) -> Optional[int]:
+def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
def find_treedef(treedef_id: Optional[int] = None):
filter_kwargs = {'name': rank_name}
if treedef_id is not None:
From c007b2916ce550a7a9c4772a30eb4a2d617ba54f Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 12:55:06 -0500
Subject: [PATCH 19/78] notes
---
specifyweb/workbench/upload/treerecord.py | 5 ++---
specifyweb/workbench/upload/upload_plan_schema.py | 3 ++-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 27493fc9d27..e6cd9e43281 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -23,7 +23,7 @@
class TreeRecord(NamedTuple):
name: str
ranks: Dict[str, Dict[str, ColumnOptions]]
- treedef_id: Optional[int] = None
+ treedef_id: Optional[int] = None # TODO: Determine if this is needed
def apply_scoping(self, collection) -> "ScopedTreeRecord":
from .scoping import apply_scoping_to_treerecord as apply_scoping
@@ -51,8 +51,7 @@ def to_json(self) -> Dict:
return {'treeRecord': result}
def unparse(self) -> Dict:
- return { 'baseTableName': self.name,
- 'uploadble': self.to_json() }
+ return { 'baseTableName': self.name, 'uploadble': self.to_json() }
class ScopedTreeRecord(NamedTuple):
name: str
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 63a31f0127a..e18e8b980e6 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -309,6 +309,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
}
)
+# TODO: Determine if it is better to return ScopedTreeRecord
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
ranks = {
rank: {'name': parse_column_options(name_or_cols)} if isinstance(name_or_cols, str)
@@ -335,7 +336,7 @@ def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional
def find_treedef(treedef_id: Optional[int] = None):
filter_kwargs = {'name': rank_name}
if treedef_id is not None:
- filter_kwargs['treedef_id'] = str(treedef_id) # or treedef_id ?
+ filter_kwargs['treedef_id'] = str(treedef_id)
return Taxontreedefitem.objects.filter(**filter_kwargs).first()
treedef = find_treedef(base_treedef_id)
From 9094d6b4c657e39e093ed41c4c1cdf3bfec255e6 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 15:08:45 -0500
Subject: [PATCH 20/78] fix find_treedef
---
.../workbench/upload/upload_plan_schema.py | 36 +++++++++++++++++--
1 file changed, 33 insertions(+), 3 deletions(-)
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index e18e8b980e6..814f87fbdf9 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -331,13 +331,19 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
ranks=ranks,
treedef_id=base_treedefid
)
-
+
def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
- def find_treedef(treedef_id: Optional[int] = None):
+ def find_treedef(treedef_id: Optional[int] = None, is_adjusting: bool = False) -> Optional['Taxontreedef']:
filter_kwargs = {'name': rank_name}
if treedef_id is not None:
filter_kwargs['treedef_id'] = str(treedef_id)
- return Taxontreedefitem.objects.filter(**filter_kwargs).first()
+ ranks = Taxontreedefitem.objects.filter(**filter_kwargs)
+ if ranks.count() == 0:
+ return None
+ elif ranks.count() > 1:
+ if is_adjusting:
+ raise ValueError(f"Multiple treedefitems with name {rank_name}")
+ return ranks.first().treedef
treedef = find_treedef(base_treedef_id)
if treedef:
@@ -351,6 +357,30 @@ def find_treedef(treedef_id: Optional[int] = None):
raise ValueError(f"Could not find treedefitem with name {rank_name}")
return None
+
+def find_treedef(
+ rank_name: str, treedef_id: Optional[int] = None, is_adjusting: bool = False
+) -> Optional["Taxontreedef"]:
+ filter_kwargs = {'name': rank_name}
+ if treedef_id is not None:
+ filter_kwargs['treedef_id'] = str(treedef_id)
+ ranks = Taxontreedefitem.objects.filter(**filter_kwargs)
+ if ranks.count() == 0:
+ return None
+ elif ranks.count() > 1 and is_adjusting and treedef_id is not None:
+ raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {treedef_id}")
+ return ranks.first().treedef
+
+def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
+ for treedef_id in [base_treedef_id, None]:
+ treedef = find_treedef(rank_name, treedef_id, is_adjusting)
+ if treedef:
+ return treedef.id
+
+ if is_adjusting:
+ raise ValueError(f"Could not find treedefitem with name {rank_name}")
+ return None
+
def adjust_upload_plan(plan: Dict) -> Dict:
if plan['baseTableName'] == 'taxon':
base_treedef_id = plan.get('taxonTreeId', None)
From 7c052d2bb3f2e133ee85dc462b734f84546caa6b Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 20 Aug 2024 15:45:21 -0500
Subject: [PATCH 21/78] fix mypy error
---
.../workbench/upload/upload_plan_schema.py | 32 ++++---------------
1 file changed, 6 insertions(+), 26 deletions(-)
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 814f87fbdf9..d754cc813ca 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -332,31 +332,6 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
treedef_id=base_treedefid
)
-def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
- def find_treedef(treedef_id: Optional[int] = None, is_adjusting: bool = False) -> Optional['Taxontreedef']:
- filter_kwargs = {'name': rank_name}
- if treedef_id is not None:
- filter_kwargs['treedef_id'] = str(treedef_id)
- ranks = Taxontreedefitem.objects.filter(**filter_kwargs)
- if ranks.count() == 0:
- return None
- elif ranks.count() > 1:
- if is_adjusting:
- raise ValueError(f"Multiple treedefitems with name {rank_name}")
- return ranks.first().treedef
-
- treedef = find_treedef(base_treedef_id)
- if treedef:
- return treedef.id
-
- treedef = find_treedef()
- if treedef:
- return treedef.id
-
- if is_adjusting:
- raise ValueError(f"Could not find treedefitem with name {rank_name}")
- return None
-
def find_treedef(
rank_name: str, treedef_id: Optional[int] = None, is_adjusting: bool = False
@@ -369,7 +344,12 @@ def find_treedef(
return None
elif ranks.count() > 1 and is_adjusting and treedef_id is not None:
raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {treedef_id}")
- return ranks.first().treedef
+
+ first_rank = ranks.first()
+ if first_rank is None:
+ return None
+
+ return first_rank.treedef
def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
for treedef_id in [base_treedef_id, None]:
From 6abe879b0c54baed8c8116004f9c2b7e53956848 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 21 Aug 2024 10:23:00 -0500
Subject: [PATCH 22/78] allow for non-background wb uploads
---
specifyweb/workbench/tasks.py | 50 ++++++++++++++++++++++-------------
specifyweb/workbench/views.py | 46 +++++++++++++++++++++-----------
2 files changed, 62 insertions(+), 34 deletions(-)
diff --git a/specifyweb/workbench/tasks.py b/specifyweb/workbench/tasks.py
index ea24552b954..295b75129bc 100644
--- a/specifyweb/workbench/tasks.py
+++ b/specifyweb/workbench/tasks.py
@@ -1,6 +1,7 @@
from typing import Optional, Any
from celery import shared_task # type: ignore
+from celery import Task # type: ignore
from celery.utils.log import get_task_logger # type: ignore
from django.db import connection, transaction
@@ -14,37 +15,50 @@
logger = get_task_logger(__name__)
-@app.task(base=LogErrorsTask, bind=True)
-def upload(self, collection_id: int, uploading_agent_id: int, ds_id: int, no_commit: bool, allow_partial: bool) -> None:
-
- def progress(current: int, total: Optional[int]) -> None:
- if not self.request.called_directly:
- self.update_state(state='PROGRESS', meta={'current': current, 'total': total})
+def upload_data(
+ collection_id: int,
+ uploading_agent_id: int,
+ ds_id: int,
+ no_commit: bool,
+ allow_partial: bool,
+ task: Optional[Task]=None,
+ progress=None,
+) -> None:
with transaction.atomic():
ds = Spdataset.objects.select_for_update().get(id=ds_id)
collection = Collection.objects.get(id=collection_id)
- if ds.uploaderstatus is None:
- logger.info("dataset is not assigned to an upload task")
- return
+ if task is not None:
+ if ds.uploaderstatus is None:
+ logger.info("dataset is not assigned to an upload task")
+ return
- if ds.uploaderstatus['taskid'] != self.request.id:
- logger.info("dataset is not assigned to this task")
- return
+ if ds.uploaderstatus['taskid'] != task.request.id:
+ logger.info("dataset is not assigned to this task")
+ return
- if not (ds.uploaderstatus['operation'] == ("validating" if no_commit else "uploading")): raise AssertionError(
- f"Invalid status '{ds.uploaderstatus['operation']}' for upload. Expected 'validating' or 'uploading'",
- {"uploadStatus" : ds.uploaderstatus['operation'],
- "operation" : "upload",
- "expectedUploadStatus" : 'validating , uploading',
- "localizationKey" : "invalidUploadStatus"})
+ if not (ds.uploaderstatus['operation'] == ("validating" if no_commit else "uploading")): raise AssertionError(
+ f"Invalid status '{ds.uploaderstatus['operation']}' for upload. Expected 'validating' or 'uploading'",
+ {"uploadStatus" : ds.uploaderstatus['operation'],
+ "operation" : "upload",
+ "expectedUploadStatus" : 'validating , uploading',
+ "localizationKey" : "invalidUploadStatus"})
do_upload_dataset(collection, uploading_agent_id, ds, no_commit, allow_partial, progress)
ds.uploaderstatus = None
ds.save(update_fields=['uploaderstatus'])
+@app.task(base=LogErrorsTask, bind=True)
+def upload(self, collection_id: int, uploading_agent_id: int, ds_id: int, no_commit: bool, allow_partial: bool) -> None:
+
+ def progress(current: int, total: Optional[int]) -> None:
+ if not self.request.called_directly:
+ self.update_state(state='PROGRESS', meta={'current': current, 'total': total})
+
+ upload_data(collection_id, uploading_agent_id, ds_id, no_commit, allow_partial, self, progress)
+
@app.task(base=LogErrorsTask, bind=True)
def unupload(self, ds_id: int, agent_id: int) -> None:
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index 70bb7270c9c..381df068b5e 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -1,5 +1,6 @@
import json
import logging
+import re
from typing import List, Optional
from uuid import uuid4
@@ -629,22 +630,35 @@ def upload(request, ds, no_commit: bool, allow_partial: bool) -> http.HttpRespon
if ds.was_uploaded():
return http.HttpResponse('dataset has already been uploaded.', status=400)
- taskid = str(uuid4())
- async_result = tasks.upload.apply_async([
- request.specify_collection.id,
- request.specify_user_agent.id,
- ds.id,
- no_commit,
- allow_partial
- ], task_id=taskid)
- ds.uploaderstatus = {
- 'operation': "validating" if no_commit else "uploading",
- 'taskid': taskid
- }
- ds.save(update_fields=['uploaderstatus'])
-
- return http.JsonResponse(async_result.id, safe=False)
-
+ data = json.loads(request.body) if request.body else {}
+ background = True
+ if 'background' in data:
+ background = bool(data['background']) # {"background": false}
+
+ if background:
+ taskid = str(uuid4())
+ async_result = tasks.upload.apply_async([
+ request.specify_collection.id,
+ request.specify_user_agent.id,
+ ds.id,
+ no_commit,
+ allow_partial
+ ], task_id=taskid)
+ ds.uploaderstatus = {
+ 'operation': "validating" if no_commit else "uploading",
+ 'taskid': taskid
+ }
+ ds.save(update_fields=['uploaderstatus'])
+ return http.JsonResponse(async_result.id, safe=False)
+ else:
+ tasks.upload_data(
+ request.specify_collection.id,
+ request.specify_user_agent.id,
+ ds.id,
+ no_commit,
+ allow_partial,
+ )
+ return http.JsonResponse(None, safe=False)
@openapi(schema={
'post': {
From 65c364d145744b7c8d4c660ef44bf8cafa2f0393 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 22 Aug 2024 11:45:07 -0500
Subject: [PATCH 23/78] change TreeRecord ranks key type to include treedefid
---
specifyweb/workbench/upload/scoping.py | 44 ++++-
specifyweb/workbench/upload/treerecord.py | 159 +++++++++++++++++-
.../workbench/upload/upload_plan_schema.py | 59 ++++++-
3 files changed, 244 insertions(+), 18 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index a36b65f47ae..2efcf38fcff 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -9,7 +9,7 @@
from .uploadable import Uploadable, ScopedUploadable
from .upload_table import UploadTable, DeferredScopeUploadTable, ScopedUploadTable, OneToOneTable, ScopedOneToOneTable
from .tomany import ToManyRecord, ScopedToManyRecord
-from .treerecord import TreeRecord, ScopedTreeRecord
+from .treerecord import TreeRank, TreeRankRecord, TreeRecord, ScopedTreeRecord
from .column_options import ColumnOptions, ExtendedColumnOptions
""" There are cases in which the scoping of records should be dependent on another record/column in a WorkBench dataset.
@@ -188,8 +188,8 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
treedef = None
if table.name == 'Taxon':
- if tr.treedef_id is not None:
- treedef = models.Taxontreedef.objects.filter(id=tr.treedef_id).first()
+ if tr.base_treedef_id is not None:
+ treedef = models.Taxontreedef.objects.filter(id=tr.base_treedef_id).first()
if treedef is None:
treedef = collection.discipline.taxontreedef
@@ -214,16 +214,50 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
treedefitems = list(treedef.treedefitems.order_by('rankid'))
treedef_ranks = [tdi.name for tdi in treedefitems]
for rank in tr.ranks:
- if rank not in treedef_ranks:
+ if rank not in treedef_ranks and isinstance(rank, TreeRankRecord) and not rank.check_rank(table.name):
raise Exception(f'"{rank}" not among {table.name} tree ranks: {treedef_ranks}')
root = list(getattr(models, table.name.capitalize()).objects.filter(definitionitem=treedefitems[0])[:1]) # assume there is only one
+ scoped_ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]] = {}
+ for r, cols in tr.ranks.items():
+ if isinstance(r, str):
+ r = TreeRank(r, table.name, treedef.id if treedef else None, tr.base_treedef_id).tree_rank_record()
+ scoped_ranks[r] = {}
+ for f, colopts in cols.items():
+ scoped_ranks[r][f] = extend_columnoptions(colopts, collection, table.name, f)
+
return ScopedTreeRecord(
name=tr.name,
- ranks={r: {f: extend_columnoptions(colopts, collection, table.name, f) for f, colopts in cols.items()} for r, cols in tr.ranks.items()},
+ # ranks={
+ # r: {
+ # f: extend_columnoptions(colopts, collection, table.name, f)
+ # for f, colopts in cols.items()
+ # }
+ # for r, cols in tr.ranks.items()
+ # },
+ ranks=scoped_ranks,
treedef=treedef,
treedefitems=treedefitems,
root=root[0] if root else None,
disambiguation={},
)
+
+# def scope_taxon_rank(rank_name: str, treedef_id: Optional[int] = None) -> Optional[int]:
+# # rank_id = None
+# treedefitems = None
+# if treedef_id is None:
+# treedefitems = models.Taxontreedefitem.objects.filter(name=rank_name, treedef_id=treedef_id)
+# else:
+# treedefitems = models.Taxontreedefitem.objects.filter(name=rank_name)
+
+# if treedefitems is None:
+# return None
+
+# if treedefitems.count() == 1:
+# # rank_id = treedefitems.first().id
+# return treedefitems.first().id
+# elif treedefitems.count() > 1:
+# raise Exception(f"Multiple treedefitems found for rank {rank_name}")
+# elif treedefitems.count() == 0:
+# return None
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index e6cd9e43281..b0b4ce12ff5 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -19,11 +19,156 @@
logger = logging.getLogger(__name__)
+class TreeRank:
+ rank_name: str
+ treedef_id: int
+ tree: str
+
+ def __init__(
+ self,
+ rank_name: str,
+ tree: str,
+ treedef_id: Optional[int] = None,
+ base_treedef_id: Optional[int] = None,
+ ):
+ self.rank_name = rank_name
+ self.treedef_id = self._get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+ self.tree = tree.lower()
+
+ def _get_treedef_id(
+ self,
+ rank_name: str,
+ tree: str,
+ treedef_id: Optional[int],
+ base_treedef_id: Optional[int],
+ ) -> int:
+ filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
+ tree_model = getattr(models, tree.lower().title() + 'treedefitem')
+ treedefitems = tree_model.objects.filter(**filter_kwargs)
+
+ if not treedefitems.exists():
+ raise ValueError(f"No treedefitems found for rank {rank_name}")
+
+ if treedefitems.count() > 1:
+ treedefitems = self._filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+
+ first_item = treedefitems.first()
+ if first_item is None:
+ raise ValueError(f"No treedefitems found for rank {rank_name}")
+
+ return first_item.treedef_id
+
+ def _build_filter_kwargs(self, rank_name: str, treedef_id: Optional[int]) -> Dict[str, Any]:
+ filter_kwargs = {'name': rank_name}
+ if treedef_id is not None:
+ filter_kwargs['treedef_id'] = treedef_id # type: ignore
+ return filter_kwargs
+
+ def _filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_id: Optional[int]):
+ if base_treedef_id is None:
+ raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
+ treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
+ if not treedefitems.exists():
+ raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+ if treedefitems.count() > 1:
+ raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+ return treedefitems
+
+ # def get_tree_model(self):
+ # return getattr(models, self.tree.lower().title() + 'treedefitem')
+
+ def check_rank(self) -> bool:
+ tree_model = getattr(models, self.tree.lower().title() + 'treedefitem')
+ rank = tree_model.objects.filter(name=self.rank_name, treedef_id=self.treedef_id)
+ return rank.exists() and rank.count() == 1
+
+ def validate_rank(self) -> None:
+ if not self.check_rank():
+ raise ValueError(f"Invalid rank {self.rank_name} for treedef {self.treedef_id}")
+
+ def tree_rank_record(self) -> 'TreeRankRecord':
+ assert self.rank_name is not None, "Rank name is required"
+ assert self.treedef_id is not None, "Treedef ID is required"
+ assert self.tree is not None and self.tree.lower() in {
+ "taxon", "storage", "geography", "geologictimeperiod", "lithostrat" # TODO: Replace with constants
+ }, "Tree is required"
+ return TreeRankRecord(self.rank_name, self.treedef_id)
+
+class TreeRankRecord(NamedTuple):
+ rank_name: str
+ treedef_id: int
+
+ def to_json(self) -> Dict:
+ return {
+ "rank": self.rank_name,
+ "treedefId": self.treedef_id,
+ }
+
+ def check_rank(self, tree: str) -> bool:
+ return TreeRank(self.rank_name, tree, self.treedef_id).check_rank()
+
+ def validate_rank(self, tree) -> None:
+ TreeRank(self.rank_name, tree, self.treedef_id).validate_rank()
+
+# def create_tree_record(rank_name: str, tree: str, treedef_id: Optional[int] = None, base_treedef_id: Optional[int] = None) -> TreeRankRecord:
+# # tree_id =
+# pass
+
+# def get_treedef_id(rank_name: str, tree: str, treedef_id: Optional[int], base_treedef_id: Optional[int]) -> int:
+# filter_kwargs = build_filter_kwargs(rank_name, treedef_id)
+# tree_model = getattr(models, tree.lower().title() + 'treedefitem')
+# treedefitems = tree_model.objects.filter(**filter_kwargs)
+
+# if not treedefitems.exists():
+# raise ValueError(f"No treedefitems found for rank {rank_name}")
+
+# if treedefitems.count() > 1:
+# treedefitems = filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+
+# first_item = treedefitems.first()
+# if first_item is None:
+# raise ValueError(f"No treedefitems found for rank {rank_name}")
+
+# return first_item.treedef_id
+
+# def build_filter_kwargs(rank_name: str, treedef_id: Optional[int]) -> Dict[str, Any]:
+# filter_kwargs = {'name': rank_name}
+# if treedef_id is not None:
+# filter_kwargs['treedef_id'] = treedef_id # type: ignore
+# return filter_kwargs
+
+# def filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_id: Optional[int]):
+# if base_treedef_id is None:
+# raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
+# treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
+# if not treedefitems.exists():
+# raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+# if treedefitems.count() > 1:
+# raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+# return treedefitems
+
+# def check_rank() -> bool:
+# rank = models.Taxontreedefitem.objects.filter(name=rank_name, treedef_id=treedef_id)
+# return rank.exists() and rank.count() == 1
+
+# def validate_rank() -> None:
+# if not self.check_rank():
+# raise ValueError(f"Invalid rank {self.rank_name} for treedef {self.treedef_id}")
+
+# def tree_rank_record() -> 'TreeRankRecord':
+# assert self.rank_name is not None, "Rank name is required"
+# assert self.treedef_id is not None, "Treedef ID is required"
+# assert self.tree is not None and self.tree.lower() in {
+# "taxon", "storage", "geography", "geologictimeperiod", "lithostrat" # TODO: Replace with constants
+# }, "Tree is required"
+# return TreeRankRecord(self.rank_name, self.treedef_id)
class TreeRecord(NamedTuple):
name: str
- ranks: Dict[str, Dict[str, ColumnOptions]]
- treedef_id: Optional[int] = None # TODO: Determine if this is needed
+ # ranks: Dict[str, Dict[str, ColumnOptions]]
+ # ranks: Dict[TreeRankRecord, Dict[str, ColumnOptions]]
+ ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]]
+ base_treedef_id: Optional[int] = None
def apply_scoping(self, collection) -> "ScopedTreeRecord":
from .scoping import apply_scoping_to_treerecord as apply_scoping
@@ -55,7 +200,7 @@ def unparse(self) -> Dict:
class ScopedTreeRecord(NamedTuple):
name: str
- ranks: Dict[str, Dict[str, ExtendedColumnOptions]]
+ ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]]
treedef: Any
treedefitems: List
root: Optional[Any]
@@ -70,15 +215,15 @@ def get_treedefs(self) -> Set:
def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: Auditor, cache: Optional[Dict]=None, row_index: Optional[int] = None) -> Union["BoundTreeRecord", ParseFailures]:
parsedFields: Dict[str, List[ParseResult]] = {}
parseFails: List[WorkBenchParseFailure] = []
- for rank, cols in self.ranks.items():
+ for tree_rank_record, cols in self.ranks.items():
nameColumn = cols['name']
- presults, pfails = parse_many(collection, self.name, cols, row)
- parsedFields[rank] = presults
+ presults, pfails = parse_many(collection, self.name, cols, row) # TODO: fix
+ parsedFields[tree_rank_record.rank_name] = presults
parseFails += pfails
filters = {k: v for result in presults for k, v in result.filter_on.items()}
if filters.get('name', None) is None:
parseFails += [
- WorkBenchParseFailure('invalidPartialRecord',{'column':nameColumn.column}, result.column)
+ WorkBenchParseFailure('invalidPartialRecord', {'column': nameColumn.column}, result.column)
for result in presults
if any(v is not None for v in result.filter_on.values())
]
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index d754cc813ca..198c2c49ff3 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -8,7 +8,7 @@
from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable
from .tomany import ToManyRecord
-from .treerecord import TreeRecord, MustMatchTreeRecord
+from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord
from .uploadable import Uploadable
from .column_options import ColumnOptions
from .scoping import DEFERRED_SCOPING
@@ -100,6 +100,7 @@
'type': 'object',
'properties': {
'treeNodeCols': { '$ref': '#/definitions/treeNodeCols' },
+ 'taxonTreeId': { '$ref': '#definitions/taxonTreeId'}
},
'required': [ 'treeNodeCols' ],
'additionalProperties': False
@@ -309,8 +310,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
}
)
-# TODO: Determine if it is better to return ScopedTreeRecord
-def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
+def parse_tree_record_old(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
ranks = {
rank: {'name': parse_column_options(name_or_cols)} if isinstance(name_or_cols, str)
else {k: parse_column_options(v) for k,v in name_or_cols['treeNodeCols'].items() }
@@ -329,16 +329,62 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
return TreeRecord(
name=table.django_name,
ranks=ranks,
- treedef_id=base_treedefid
+ base_treedef_id=base_treedefid
)
+def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
+ ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
+ for rank, name_or_cols in to_parse['ranks'].items():
+ if isinstance(name_or_cols, str):
+ rank_name = name_or_cols
+ parsed_cols = {'name': parse_column_options(name_or_cols)}
+ else:
+ tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
+ rank_name = name_or_cols['treeNodeCols']['name']
+ parsed_cols = tree_node_cols
+
+ treedefid = get_treedef_id(rank_name, False, base_treedefid)
+ tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ ranks[tree_rank_record] = parsed_cols
+
+ # for rank, name_or_cols in to_parse['ranks'].items():
+ # if isinstance(name_or_cols, str):
+ # rank_name = name_or_cols
+ # treedefid = get_treedef_id(rank_name, False, base_treedefid)
+ # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ # ranks[tree_rank_record] = {'name': parse_column_options(name_or_cols)}
+ # else:
+ # tree_node_cols = {}
+ # for k, v in name_or_cols['treeNodeCols'].items():
+ # tree_node_cols[k] = parse_column_options(v)
+
+ # rank_name = name_or_cols['treeNodeCols']['name']
+ # treedefid = get_treedef_id(rank_name, False, base_treedefid)
+ # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ # ranks[tree_rank_record] = tree_node_cols
+
+ for rank, cols in ranks.items():
+ assert 'name' in cols, to_parse
+
+ if base_treedefid is None:
+ for tree_rank_record, cols in ranks.items(): # type: ignore
+ treedefid = get_treedef_id(cols['name'].column, False, base_treedefid)
+ if treedefid is not None:
+ base_treedefid = treedefid
+ break
+
+ return TreeRecord(
+ name=table.django_name,
+ ranks=ranks,
+ base_treedef_id=base_treedefid
+ )
def find_treedef(
rank_name: str, treedef_id: Optional[int] = None, is_adjusting: bool = False
) -> Optional["Taxontreedef"]:
filter_kwargs = {'name': rank_name}
if treedef_id is not None:
- filter_kwargs['treedef_id'] = str(treedef_id)
+ filter_kwargs['treedef_id'] = treedef_id # type: ignore
ranks = Taxontreedefitem.objects.filter(**filter_kwargs)
if ranks.count() == 0:
return None
@@ -368,7 +414,8 @@ def adjust_upload_plan(plan: Dict) -> Dict:
if isinstance(name_or_cols, dict):
rank_name = name_or_cols['treeNodeCols']['name']
if 'treedefid' not in name_or_cols:
- name_or_cols['treedefid'] = str(get_treedef_id(rank_name, True, base_treedef_id))
+ # name_or_cols['treedefid'] = str(get_treedef_id(rank_name, True, base_treedef_id))
+ name_or_cols['taxonTreeId'] = get_treedef_id(rank_name, True, base_treedef_id)
return plan
From 7552b87310091b849bafcae7a37ab3f63e916d48 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 22 Aug 2024 12:06:07 -0500
Subject: [PATCH 24/78] clearnup and fix unparsing unit test
---
specifyweb/workbench/upload/scoping.py | 26 --------
specifyweb/workbench/upload/treerecord.py | 63 ++-----------------
.../workbench/upload/upload_plan_schema.py | 38 -----------
3 files changed, 5 insertions(+), 122 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 2efcf38fcff..848977c3b9a 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -229,35 +229,9 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
return ScopedTreeRecord(
name=tr.name,
- # ranks={
- # r: {
- # f: extend_columnoptions(colopts, collection, table.name, f)
- # for f, colopts in cols.items()
- # }
- # for r, cols in tr.ranks.items()
- # },
ranks=scoped_ranks,
treedef=treedef,
treedefitems=treedefitems,
root=root[0] if root else None,
disambiguation={},
)
-
-# def scope_taxon_rank(rank_name: str, treedef_id: Optional[int] = None) -> Optional[int]:
-# # rank_id = None
-# treedefitems = None
-# if treedef_id is None:
-# treedefitems = models.Taxontreedefitem.objects.filter(name=rank_name, treedef_id=treedef_id)
-# else:
-# treedefitems = models.Taxontreedefitem.objects.filter(name=rank_name)
-
-# if treedefitems is None:
-# return None
-
-# if treedefitems.count() == 1:
-# # rank_id = treedefitems.first().id
-# return treedefitems.first().id
-# elif treedefitems.count() > 1:
-# raise Exception(f"Multiple treedefitems found for rank {rank_name}")
-# elif treedefitems.count() == 0:
-# return None
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index b0b4ce12ff5..55f77413e18 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -73,9 +73,6 @@ def _filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_
if treedefitems.count() > 1:
raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
return treedefitems
-
- # def get_tree_model(self):
- # return getattr(models, self.tree.lower().title() + 'treedefitem')
def check_rank(self) -> bool:
tree_model = getattr(models, self.tree.lower().title() + 'treedefitem')
@@ -104,69 +101,19 @@ def to_json(self) -> Dict:
"treedefId": self.treedef_id,
}
+ def to_key(self) -> Tuple[str, int]:
+ return (self.rank_name, self.treedef_id)
+
def check_rank(self, tree: str) -> bool:
return TreeRank(self.rank_name, tree, self.treedef_id).check_rank()
def validate_rank(self, tree) -> None:
TreeRank(self.rank_name, tree, self.treedef_id).validate_rank()
-# def create_tree_record(rank_name: str, tree: str, treedef_id: Optional[int] = None, base_treedef_id: Optional[int] = None) -> TreeRankRecord:
-# # tree_id =
-# pass
-
-# def get_treedef_id(rank_name: str, tree: str, treedef_id: Optional[int], base_treedef_id: Optional[int]) -> int:
-# filter_kwargs = build_filter_kwargs(rank_name, treedef_id)
-# tree_model = getattr(models, tree.lower().title() + 'treedefitem')
-# treedefitems = tree_model.objects.filter(**filter_kwargs)
-
-# if not treedefitems.exists():
-# raise ValueError(f"No treedefitems found for rank {rank_name}")
-
-# if treedefitems.count() > 1:
-# treedefitems = filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
-
-# first_item = treedefitems.first()
-# if first_item is None:
-# raise ValueError(f"No treedefitems found for rank {rank_name}")
-
-# return first_item.treedef_id
-
-# def build_filter_kwargs(rank_name: str, treedef_id: Optional[int]) -> Dict[str, Any]:
-# filter_kwargs = {'name': rank_name}
-# if treedef_id is not None:
-# filter_kwargs['treedef_id'] = treedef_id # type: ignore
-# return filter_kwargs
-
-# def filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_id: Optional[int]):
-# if base_treedef_id is None:
-# raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
-# treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
-# if not treedefitems.exists():
-# raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
-# if treedefitems.count() > 1:
-# raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
-# return treedefitems
-
-# def check_rank() -> bool:
-# rank = models.Taxontreedefitem.objects.filter(name=rank_name, treedef_id=treedef_id)
-# return rank.exists() and rank.count() == 1
-
-# def validate_rank() -> None:
-# if not self.check_rank():
-# raise ValueError(f"Invalid rank {self.rank_name} for treedef {self.treedef_id}")
-
-# def tree_rank_record() -> 'TreeRankRecord':
-# assert self.rank_name is not None, "Rank name is required"
-# assert self.treedef_id is not None, "Treedef ID is required"
-# assert self.tree is not None and self.tree.lower() in {
-# "taxon", "storage", "geography", "geologictimeperiod", "lithostrat" # TODO: Replace with constants
-# }, "Tree is required"
-# return TreeRankRecord(self.rank_name, self.treedef_id)
-
class TreeRecord(NamedTuple):
name: str
# ranks: Dict[str, Dict[str, ColumnOptions]]
- # ranks: Dict[TreeRankRecord, Dict[str, ColumnOptions]]
+ # ranks: Dict[Union[str, Tuple[str, int]], Dict[str, ColumnOptions]]
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]]
base_treedef_id: Optional[int] = None
@@ -180,7 +127,7 @@ def get_cols(self) -> Set[str]:
def to_json(self) -> Dict:
result = {
"ranks": {
- rank: (
+ rank.rank_name if isinstance(rank, TreeRankRecord) else rank: (
cols["name"].to_json()
if len(cols) == 1
else dict(
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 198c2c49ff3..7c50ae8f0bb 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -310,28 +310,6 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
}
)
-def parse_tree_record_old(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
- ranks = {
- rank: {'name': parse_column_options(name_or_cols)} if isinstance(name_or_cols, str)
- else {k: parse_column_options(v) for k,v in name_or_cols['treeNodeCols'].items() }
- for rank, name_or_cols in to_parse['ranks'].items()
- }
- for rank, cols in ranks.items():
- assert 'name' in cols, to_parse
-
- if base_treedefid is None:
- for rank, cols in ranks.items():
- treedefid = get_treedef_id(cols['name'].column, False, base_treedefid)
- if treedefid is not None:
- base_treedefid = treedefid
- break
-
- return TreeRecord(
- name=table.django_name,
- ranks=ranks,
- base_treedef_id=base_treedefid
- )
-
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
for rank, name_or_cols in to_parse['ranks'].items():
@@ -347,22 +325,6 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
ranks[tree_rank_record] = parsed_cols
- # for rank, name_or_cols in to_parse['ranks'].items():
- # if isinstance(name_or_cols, str):
- # rank_name = name_or_cols
- # treedefid = get_treedef_id(rank_name, False, base_treedefid)
- # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
- # ranks[tree_rank_record] = {'name': parse_column_options(name_or_cols)}
- # else:
- # tree_node_cols = {}
- # for k, v in name_or_cols['treeNodeCols'].items():
- # tree_node_cols[k] = parse_column_options(v)
-
- # rank_name = name_or_cols['treeNodeCols']['name']
- # treedefid = get_treedef_id(rank_name, False, base_treedefid)
- # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
- # ranks[tree_rank_record] = tree_node_cols
-
for rank, cols in ranks.items():
assert 'name' in cols, to_parse
From 4eb6730cdcbd1da3aaca87992cdaf10aec861b37 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 23 Aug 2024 14:24:10 -0500
Subject: [PATCH 25/78] reformulate and change taxonTreeId to treeId in schema
---
.../workbench/upload/tests/example_plan.py | 2 +
specifyweb/workbench/upload/treerecord.py | 46 +++--
.../workbench/upload/upload_plan_schema.py | 177 ++++++++++++------
specifyweb/workbench/views.py | 4 +-
4 files changed, 149 insertions(+), 80 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index 7aabb873bcb..fcc3277aa54 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -45,12 +45,14 @@
'name': 'Species',
'author': 'Species Author',
},
+ treeId = 3
),
'Subspecies': dict(
treeNodeCols = {
'name': 'Subspecies',
'author': 'Subspecies Author',
},
+ treeId = 3
),
}
)}
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 55f77413e18..0aa77872cdf 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -43,7 +43,8 @@ def _get_treedef_id(
base_treedef_id: Optional[int],
) -> int:
filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
- tree_model = getattr(models, tree.lower().title() + 'treedefitem')
+ # tree_model = getattr(models, tree.lower().title() + 'treedefitem')
+ tree_model = get_tree_model(tree)
treedefitems = tree_model.objects.filter(**filter_kwargs)
if not treedefitems.exists():
@@ -75,7 +76,8 @@ def _filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_
return treedefitems
def check_rank(self) -> bool:
- tree_model = getattr(models, self.tree.lower().title() + 'treedefitem')
+ # tree_model = getattr(models, self.tree.lower().title() + 'treedefitem')
+ tree_model = get_tree_model(self.tree)
rank = tree_model.objects.filter(name=self.rank_name, treedef_id=self.treedef_id)
return rank.exists() and rank.count() == 1
@@ -91,6 +93,14 @@ def tree_rank_record(self) -> 'TreeRankRecord':
}, "Tree is required"
return TreeRankRecord(self.rank_name, self.treedef_id)
+def get_tree_model(tree: str):
+ return getattr(models, tree.lower().title() + 'treedefitem')
+
+# def is_rank_identifiable(rank: str, tree: str, treedef_id: int) -> bool:
+# tree_model = get_tree_model(tree)
+# results = tree_model.objects.filter(name=rank, treedef_id=treedef_id)
+# return results.exists() and results.count() == 1
+
class TreeRankRecord(NamedTuple):
rank_name: str
treedef_id: int
@@ -112,8 +122,6 @@ def validate_rank(self, tree) -> None:
class TreeRecord(NamedTuple):
name: str
- # ranks: Dict[str, Dict[str, ColumnOptions]]
- # ranks: Dict[Union[str, Tuple[str, int]], Dict[str, ColumnOptions]]
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]]
base_treedef_id: Optional[int] = None
@@ -125,21 +133,21 @@ def get_cols(self) -> Set[str]:
return {col.column for r in self.ranks.values() for col in r.values() if hasattr(col, 'column')}
def to_json(self) -> Dict:
- result = {
- "ranks": {
- rank.rank_name if isinstance(rank, TreeRankRecord) else rank: (
- cols["name"].to_json()
- if len(cols) == 1
- else dict(
- treeNodeCols={
- k: v.to_json() if hasattr(v, "to_json") else v
- for k, v in cols.items()
- }
- )
- )
- for rank, cols in self.ranks.items()
- },
- }
+ # result: Dict[str, Union[str, int, Dict[str, Any]]] = {"ranks": {}}
+ result = {"ranks": {}} # type: ignore
+
+ for rank, cols in self.ranks.items():
+ rank_key = rank.rank_name if isinstance(rank, TreeRankRecord) else rank
+ treeNodeCols = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in cols.items()}
+
+ if len(cols) == 1:
+ result["ranks"][rank_key] = treeNodeCols["name"]
+ else:
+ rank_data = {"treeNodeCols": treeNodeCols}
+ if isinstance(rank, TreeRankRecord):
+ rank_data["treeId"] = rank.treedef_id # type: ignore
+ result["ranks"][rank_key] = rank_data
+
return {'treeRecord': result}
def unparse(self) -> Dict:
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 7c50ae8f0bb..14a246dd912 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -3,12 +3,10 @@
import logging
from specifyweb.specify.datamodel import datamodel, Table, Relationship
-from specifyweb.specify.load_datamodel import DoesNotExistError
-from specifyweb.specify.models import Taxontreedefitem, Taxontreedef
from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable
from .tomany import ToManyRecord
-from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord
+from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord, get_tree_model
from .uploadable import Uploadable
from .column_options import ColumnOptions
from .scoping import DEFERRED_SCOPING
@@ -37,7 +35,7 @@
},
]
},
- "taxonTreeId": {
+ "treeId": {
"oneOf": [
{ "type": "integer" },
{ "type": "null" }
@@ -100,7 +98,7 @@
'type': 'object',
'properties': {
'treeNodeCols': { '$ref': '#/definitions/treeNodeCols' },
- 'taxonTreeId': { '$ref': '#definitions/taxonTreeId'}
+ 'treeId': { '$ref': '#definitions/treeId'}
},
'required': [ 'treeNodeCols' ],
'additionalProperties': False
@@ -150,7 +148,7 @@
],
},
- 'taxonTreeId': {
+ 'treeId': {
"oneOf": [
{ "type": "integer" },
{ "type": "null" }
@@ -235,8 +233,8 @@
def parse_plan_with_basetable(collection, to_parse: Dict) -> Tuple[Table, Uploadable]:
base_table = datamodel.get_table_strict(to_parse['baseTableName'])
extra = {}
- if 'taxonTreeId' in to_parse.keys():
- extra['treedefid'] = to_parse['taxonTreeId']
+ if 'treeId' in to_parse.keys():
+ extra['treedefid'] = to_parse['treeId']
return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'], extra)
def parse_plan(collection, to_parse: Dict) -> Uploadable:
@@ -244,7 +242,7 @@ def parse_plan(collection, to_parse: Dict) -> Uploadable:
def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> Uploadable:
if 'uploadTable' in to_parse:
- return parse_upload_table(collection, table, to_parse['uploadTable'])
+ return parse_upload_table(collection, table, to_parse['uploadTable'], extra)
if 'oneToOneTable' in to_parse:
return OneToOneTable(*parse_upload_table(collection, table, to_parse['oneToOneTable']))
@@ -263,7 +261,7 @@ def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {})
raise ValueError('unknown uploadable type')
-def parse_upload_table(collection, table: Table, to_parse: Dict) -> UploadTable:
+def parse_upload_table(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> UploadTable:
def rel_table(key: str) -> Table:
return datamodel.get_table_strict(table.get_relationship(key).relatedModelName)
@@ -288,7 +286,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
for key, to_one in to_parse['toOne'].items()
},
toMany={
- key: [parse_to_many_record(default_collection, rel_table(key), record) for record in to_manys]
+ key: [parse_to_many_record(default_collection, rel_table(key), records) for record in to_manys]
for key, to_manys in to_parse['toMany'].items()
}
)
@@ -305,7 +303,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
for key, to_one in to_parse['toOne'].items()
},
toMany={
- key: [parse_to_many_record(collection, rel_table(key), record) for record in to_manys]
+ key: [parse_to_many_record(collection, rel_table(key), record, extra) for record in to_manys]
for key, to_manys in to_parse['toMany'].items()
}
)
@@ -313,6 +311,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
for rank, name_or_cols in to_parse['ranks'].items():
+ treedefid = None
if isinstance(name_or_cols, str):
rank_name = name_or_cols
parsed_cols = {'name': parse_column_options(name_or_cols)}
@@ -320,20 +319,29 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
rank_name = name_or_cols['treeNodeCols']['name']
parsed_cols = tree_node_cols
+ if 'treeId' in name_or_cols:
+ treedefid = name_or_cols['treeId']
- treedefid = get_treedef_id(rank_name, False, base_treedefid)
- tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ # if treedefid is None:
+ # treedefid = get_treedef_id(rank_name, table.name, False, base_treedefid)
+ tree_rank_record = TreeRank(rank_name, table.name, treedefid, base_treedefid).tree_rank_record()
ranks[tree_rank_record] = parsed_cols
+ if not isinstance(name_or_cols, str): # TODO: Make better
+ to_parse['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
+
+ if base_treedefid is None:
+ base_treedefid = tree_rank_record.treedef_id
for rank, cols in ranks.items():
assert 'name' in cols, to_parse
- if base_treedefid is None:
- for tree_rank_record, cols in ranks.items(): # type: ignore
- treedefid = get_treedef_id(cols['name'].column, False, base_treedefid)
- if treedefid is not None:
- base_treedefid = treedefid
- break
+ # if base_treedefid is None:
+ # for tree_rank_record, cols in ranks.items(): # type: ignore
+ # # treedefid = get_treedef_id(cols['name'].column, table.name, False, base_treedefid)
+ # treedefid =
+ # if treedefid is not None:
+ # base_treedefid = treedefid
+ # break
return TreeRecord(
name=table.django_name,
@@ -341,47 +349,100 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
base_treedef_id=base_treedefid
)
-def find_treedef(
- rank_name: str, treedef_id: Optional[int] = None, is_adjusting: bool = False
-) -> Optional["Taxontreedef"]:
- filter_kwargs = {'name': rank_name}
- if treedef_id is not None:
- filter_kwargs['treedef_id'] = treedef_id # type: ignore
- ranks = Taxontreedefitem.objects.filter(**filter_kwargs)
- if ranks.count() == 0:
- return None
- elif ranks.count() > 1 and is_adjusting and treedef_id is not None:
- raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {treedef_id}")
+# def find_treedef(rank_name: str, tree: str, treedef_id: Optional[int] = None, is_adjusting: bool = False):
+# tree_model = get_tree_model(tree)
+# filter_kwargs = {'name': rank_name}
+# if treedef_id is not None:
+# filter_kwargs['treedef_id'] = treedef_id # type: ignore
+# ranks = tree_model.objects.filter(**filter_kwargs)
+# if ranks.count() == 0:
+# return None
+# elif ranks.count() > 1 and is_adjusting and treedef_id is not None:
+# raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {treedef_id}")
+
+# first_rank = ranks.first()
+# if first_rank is None:
+# return None
+
+# return first_rank.treedef
+
+# def get_treedef_id_old(
+# rank_name: str, tree: str, is_adjusting: bool, base_treedef_id: Optional[int] = None
+# ) -> Optional[int]:
+# for treedef_id in [base_treedef_id, None]:
+# treedef = find_treedef(rank_name, tree, treedef_id, is_adjusting)
+# if treedef:
+# return treedef.id
+
+# if is_adjusting:
+# raise ValueError(f"Could not find treedefitem with name {rank_name}")
+# return None
+
+# def get_treedef_id(
+# rank_name: str, tree: str, is_adjusting: bool, base_treedef_id: Optional[int] = None
+# ) -> Optional[int]:
+# treedef = find_treedef(rank_name, tree, base_treedef_id, False)
+
+# if treedef is None:
+# treedef = find_treedef(rank_name, tree, None, is_adjusting)
+
+# if is_adjusting:
+# raise ValueError(f"Could not find treedefitem with name {rank_name}")
+# return None
+
+# def adjust_upload_plan(plan: Dict, collection) -> Dict:
+# if (
+# "uploadable" not in plan
+# or "treeRecord" not in plan["uploadable"]
+# or "ranks" not in plan["uploadable"]["treeRecord"]
+# ):
+# return plan
+
+# base_table = datamodel.get_table_strict(plan['baseTableName'])
+# uploadable = parse_uploadable(collection, base_table, plan['uploadable'])
+# tree_table_name = plan['uploadable']['uploadTable']['toMany']
+
+# # determinations = plan['uploadable']['uploadTable']['toMany']['determinations']
+# # for determination in determinations:
+# tree = plan['uploadable']['uploadTable']['toMany']['determinations'][0]['toOne'].keys()[0]
- first_rank = ranks.first()
- if first_rank is None:
- return None
+# base_treedef_id = plan.get('treeId', None)
+# tree = 'Taxon' # tree = plan['uploadable']
+# for rank, name_or_cols in plan['uploadable']['treeRecord']['ranks'].items():
+# if isinstance(name_or_cols, dict):
+# rank_name = name_or_cols['treeNodeCols']['name']
+# if 'treedefid' not in name_or_cols:
+# # name_or_cols['treeId'] = get_treedef_id(rank_name, tree, True, base_treedef_id)
+# name_or_cols['treeId'] = TreeRank(rank_name, tree, None, base_treedef_id).treedef_id
+
+# return plan
+
+def adjust_upload_plan(plan: Dict, collection):
+ base_table = datamodel.get_table_strict(plan['baseTableName'])
+ extra = {}
+ if 'treeId' in plan.keys():
+ extra['treedefid'] = plan['treeId']
- return first_rank.treedef
-
-def get_treedef_id(rank_name: str, is_adjusting: bool, base_treedef_id: Optional[int] = None) -> Optional[int]:
- for treedef_id in [base_treedef_id, None]:
- treedef = find_treedef(rank_name, treedef_id, is_adjusting)
- if treedef:
- return treedef.id
-
- if is_adjusting:
- raise ValueError(f"Could not find treedefitem with name {rank_name}")
- return None
-
-def adjust_upload_plan(plan: Dict) -> Dict:
- if plan['baseTableName'] == 'taxon':
- base_treedef_id = plan.get('taxonTreeId', None)
- for rank, name_or_cols in plan['uploadable']['treeRecord']['ranks'].items():
- if isinstance(name_or_cols, dict):
- rank_name = name_or_cols['treeNodeCols']['name']
- if 'treedefid' not in name_or_cols:
- # name_or_cols['treedefid'] = str(get_treedef_id(rank_name, True, base_treedef_id))
- name_or_cols['taxonTreeId'] = get_treedef_id(rank_name, True, base_treedef_id)
-
+ if 'treeRecord' in plan:
+ treedefid = None
+ if 'treedefid' in extra.keys():
+ treedefid = int(extra['treedefid'])
+
+ for rank, name_or_cols in plan['ranks'].items():
+ treedefid = None
+ if not isinstance(name_or_cols, str):
+ tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
+ rank_name = name_or_cols['treeNodeCols']['name']
+ parsed_cols = tree_node_cols
+ if 'treeId' in name_or_cols:
+ treedefid = name_or_cols['treeId']
+ # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ # plan['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
+
return plan
+
-def parse_to_many_record(collection, table: Table, to_parse: Dict) -> ToManyRecord:
+def parse_to_many_record(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> ToManyRecord:
def rel_table(key: str) -> Table:
return datamodel.get_table_strict(table.get_relationship(key).relatedModelName)
@@ -391,7 +452,7 @@ def rel_table(key: str) -> Table:
wbcols={k: parse_column_options(v) for k,v in to_parse['wbcols'].items()},
static=to_parse['static'],
toOne={
- key: parse_uploadable(collection, rel_table(key), to_one)
+ key: parse_uploadable(collection, rel_table(key), to_one, extra)
for key, to_one in to_parse['toOne'].items()
},
)
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index 381df068b5e..cd852c850e1 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -1,6 +1,5 @@
import json
import logging
-import re
from typing import List, Optional
from uuid import uuid4
@@ -484,8 +483,7 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
parsed_plan = upload_plan_schema.parse_plan(request.specify_collection, plan)
new_cols = parsed_plan.get_cols() - set(ds.columns)
- if plan['baseTableName'] == 'taxon':
- plan = upload_plan_schema.adjust_upload_plan(plan)
+ # plan = upload_plan_schema.adjust_upload_plan(plan, request.specify_collection)
if new_cols:
ncols = len(ds.columns)
ds.columns += list(new_cols)
From cc62e3fae0fa20bf06cec0743801b8fbee1d335e Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 23 Aug 2024 15:18:10 -0500
Subject: [PATCH 26/78] remove adjust_upload_plan
---
specifyweb/workbench/upload/treerecord.py | 7 -
.../workbench/upload/upload_plan_schema.py | 130 ++----------------
2 files changed, 13 insertions(+), 124 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 0aa77872cdf..b4e8e4c6d9b 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -43,7 +43,6 @@ def _get_treedef_id(
base_treedef_id: Optional[int],
) -> int:
filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
- # tree_model = getattr(models, tree.lower().title() + 'treedefitem')
tree_model = get_tree_model(tree)
treedefitems = tree_model.objects.filter(**filter_kwargs)
@@ -76,7 +75,6 @@ def _filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_
return treedefitems
def check_rank(self) -> bool:
- # tree_model = getattr(models, self.tree.lower().title() + 'treedefitem')
tree_model = get_tree_model(self.tree)
rank = tree_model.objects.filter(name=self.rank_name, treedef_id=self.treedef_id)
return rank.exists() and rank.count() == 1
@@ -96,11 +94,6 @@ def tree_rank_record(self) -> 'TreeRankRecord':
def get_tree_model(tree: str):
return getattr(models, tree.lower().title() + 'treedefitem')
-# def is_rank_identifiable(rank: str, tree: str, treedef_id: int) -> bool:
-# tree_model = get_tree_model(tree)
-# results = tree_model.objects.filter(name=rank, treedef_id=treedef_id)
-# return results.exists() and results.count() == 1
-
class TreeRankRecord(NamedTuple):
rank_name: str
treedef_id: int
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 14a246dd912..070fcfa66be 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -286,7 +286,7 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
for key, to_one in to_parse['toOne'].items()
},
toMany={
- key: [parse_to_many_record(default_collection, rel_table(key), records) for record in to_manys]
+ key: [parse_to_many_record(default_collection, rel_table(key), record) for record in to_manys]
for key, to_manys in to_parse['toMany'].items()
}
)
@@ -309,24 +309,21 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
)
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
+ def parse_rank(name_or_cols):
+ if isinstance(name_or_cols, str):
+ return name_or_cols, {'name': parse_column_options(name_or_cols)}, None
+ tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
+ rank_name = name_or_cols['treeNodeCols']['name']
+ treedefid = name_or_cols.get('treeId')
+ return rank_name, tree_node_cols, treedefid
+
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
for rank, name_or_cols in to_parse['ranks'].items():
- treedefid = None
- if isinstance(name_or_cols, str):
- rank_name = name_or_cols
- parsed_cols = {'name': parse_column_options(name_or_cols)}
- else:
- tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
- rank_name = name_or_cols['treeNodeCols']['name']
- parsed_cols = tree_node_cols
- if 'treeId' in name_or_cols:
- treedefid = name_or_cols['treeId']
-
- # if treedefid is None:
- # treedefid = get_treedef_id(rank_name, table.name, False, base_treedefid)
- tree_rank_record = TreeRank(rank_name, table.name, treedefid, base_treedefid).tree_rank_record()
+ rank_name, parsed_cols, treedefid = parse_rank(name_or_cols)
+ tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
ranks[tree_rank_record] = parsed_cols
- if not isinstance(name_or_cols, str): # TODO: Make better
+
+ if not isinstance(name_or_cols, str):
to_parse['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
if base_treedefid is None:
@@ -335,113 +332,12 @@ def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid:
for rank, cols in ranks.items():
assert 'name' in cols, to_parse
- # if base_treedefid is None:
- # for tree_rank_record, cols in ranks.items(): # type: ignore
- # # treedefid = get_treedef_id(cols['name'].column, table.name, False, base_treedefid)
- # treedefid =
- # if treedefid is not None:
- # base_treedefid = treedefid
- # break
-
return TreeRecord(
name=table.django_name,
ranks=ranks,
base_treedef_id=base_treedefid
)
-# def find_treedef(rank_name: str, tree: str, treedef_id: Optional[int] = None, is_adjusting: bool = False):
-# tree_model = get_tree_model(tree)
-# filter_kwargs = {'name': rank_name}
-# if treedef_id is not None:
-# filter_kwargs['treedef_id'] = treedef_id # type: ignore
-# ranks = tree_model.objects.filter(**filter_kwargs)
-# if ranks.count() == 0:
-# return None
-# elif ranks.count() > 1 and is_adjusting and treedef_id is not None:
-# raise ValueError(f"Multiple treedefitems with name {rank_name} and treedef_id {treedef_id}")
-
-# first_rank = ranks.first()
-# if first_rank is None:
-# return None
-
-# return first_rank.treedef
-
-# def get_treedef_id_old(
-# rank_name: str, tree: str, is_adjusting: bool, base_treedef_id: Optional[int] = None
-# ) -> Optional[int]:
-# for treedef_id in [base_treedef_id, None]:
-# treedef = find_treedef(rank_name, tree, treedef_id, is_adjusting)
-# if treedef:
-# return treedef.id
-
-# if is_adjusting:
-# raise ValueError(f"Could not find treedefitem with name {rank_name}")
-# return None
-
-# def get_treedef_id(
-# rank_name: str, tree: str, is_adjusting: bool, base_treedef_id: Optional[int] = None
-# ) -> Optional[int]:
-# treedef = find_treedef(rank_name, tree, base_treedef_id, False)
-
-# if treedef is None:
-# treedef = find_treedef(rank_name, tree, None, is_adjusting)
-
-# if is_adjusting:
-# raise ValueError(f"Could not find treedefitem with name {rank_name}")
-# return None
-
-# def adjust_upload_plan(plan: Dict, collection) -> Dict:
-# if (
-# "uploadable" not in plan
-# or "treeRecord" not in plan["uploadable"]
-# or "ranks" not in plan["uploadable"]["treeRecord"]
-# ):
-# return plan
-
-# base_table = datamodel.get_table_strict(plan['baseTableName'])
-# uploadable = parse_uploadable(collection, base_table, plan['uploadable'])
-# tree_table_name = plan['uploadable']['uploadTable']['toMany']
-
-# # determinations = plan['uploadable']['uploadTable']['toMany']['determinations']
-# # for determination in determinations:
-# tree = plan['uploadable']['uploadTable']['toMany']['determinations'][0]['toOne'].keys()[0]
-
-# base_treedef_id = plan.get('treeId', None)
-# tree = 'Taxon' # tree = plan['uploadable']
-# for rank, name_or_cols in plan['uploadable']['treeRecord']['ranks'].items():
-# if isinstance(name_or_cols, dict):
-# rank_name = name_or_cols['treeNodeCols']['name']
-# if 'treedefid' not in name_or_cols:
-# # name_or_cols['treeId'] = get_treedef_id(rank_name, tree, True, base_treedef_id)
-# name_or_cols['treeId'] = TreeRank(rank_name, tree, None, base_treedef_id).treedef_id
-
-# return plan
-
-def adjust_upload_plan(plan: Dict, collection):
- base_table = datamodel.get_table_strict(plan['baseTableName'])
- extra = {}
- if 'treeId' in plan.keys():
- extra['treedefid'] = plan['treeId']
-
- if 'treeRecord' in plan:
- treedefid = None
- if 'treedefid' in extra.keys():
- treedefid = int(extra['treedefid'])
-
- for rank, name_or_cols in plan['ranks'].items():
- treedefid = None
- if not isinstance(name_or_cols, str):
- tree_node_cols = {k: parse_column_options(v) for k, v in name_or_cols['treeNodeCols'].items()}
- rank_name = name_or_cols['treeNodeCols']['name']
- parsed_cols = tree_node_cols
- if 'treeId' in name_or_cols:
- treedefid = name_or_cols['treeId']
- # tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
- # plan['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
-
- return plan
-
-
def parse_to_many_record(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> ToManyRecord:
def rel_table(key: str) -> Table:
From e78e950dcd8e90c3e2fdd23e4940c34ffa6c5e32 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 23 Aug 2024 17:35:40 -0500
Subject: [PATCH 27/78] avoid schema adjustment for now
---
specifyweb/workbench/upload/tests/example_plan.py | 1 +
specifyweb/workbench/upload/upload_plan_schema.py | 5 +++--
specifyweb/workbench/views.py | 1 -
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index fcc3277aa54..d3b21a7bae8 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -2,6 +2,7 @@
from ..tomany import ToManyRecord
from ..treerecord import TreeRecord
from ..upload_plan_schema import parse_column_options
+# from specifyweb.specify import models as spmodels
json = dict(
baseTableName = 'Collectionobject',
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 070fcfa66be..3c2f4171038 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -323,8 +323,9 @@ def parse_rank(name_or_cols):
tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
ranks[tree_rank_record] = parsed_cols
- if not isinstance(name_or_cols, str):
- to_parse['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
+ # Update the schema with the treeId
+ # if not isinstance(name_or_cols, str):
+ # to_parse['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
if base_treedefid is None:
base_treedefid = tree_rank_record.treedef_id
diff --git a/specifyweb/workbench/views.py b/specifyweb/workbench/views.py
index cd852c850e1..a1f94d3e2d0 100644
--- a/specifyweb/workbench/views.py
+++ b/specifyweb/workbench/views.py
@@ -483,7 +483,6 @@ def dataset(request, ds: models.Spdataset) -> http.HttpResponse:
parsed_plan = upload_plan_schema.parse_plan(request.specify_collection, plan)
new_cols = parsed_plan.get_cols() - set(ds.columns)
- # plan = upload_plan_schema.adjust_upload_plan(plan, request.specify_collection)
if new_cols:
ncols = len(ds.columns)
ds.columns += list(new_cols)
From 1ab212c90f47320539880df2bf016c383bbfa586 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Mon, 26 Aug 2024 10:48:02 -0500
Subject: [PATCH 28/78] fix _get_treedef_id
---
.../workbench/upload/tests/example_plan.py | 4 ++--
specifyweb/workbench/upload/treerecord.py | 16 +++++++++-------
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index d3b21a7bae8..0dd9f50fbe2 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -46,14 +46,14 @@
'name': 'Species',
'author': 'Species Author',
},
- treeId = 3
+ treeId = 142 # TODO: Set this dynamically
),
'Subspecies': dict(
treeNodeCols = {
'name': 'Subspecies',
'author': 'Subspecies Author',
},
- treeId = 3
+ treeId = 142 # TODO: Set this dynamically
),
}
)}
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index b4e8e4c6d9b..bd257e3e84e 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -42,12 +42,14 @@ def _get_treedef_id(
treedef_id: Optional[int],
base_treedef_id: Optional[int],
) -> int:
- filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
tree_model = get_tree_model(tree)
- treedefitems = tree_model.objects.filter(**filter_kwargs)
- if not treedefitems.exists():
- raise ValueError(f"No treedefitems found for rank {rank_name}")
+ filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
+ treedefitems = tree_model.objects.filter(**filter_kwargs)
+
+ if not treedefitems.exists() and treedef_id is not None:
+ filter_kwargs = self._build_filter_kwargs(rank_name)
+ treedefitems = tree_model.objects.filter(**filter_kwargs)
if treedefitems.count() > 1:
treedefitems = self._filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
@@ -58,7 +60,7 @@ def _get_treedef_id(
return first_item.treedef_id
- def _build_filter_kwargs(self, rank_name: str, treedef_id: Optional[int]) -> Dict[str, Any]:
+ def _build_filter_kwargs(self, rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
filter_kwargs = {'name': rank_name}
if treedef_id is not None:
filter_kwargs['treedef_id'] = treedef_id # type: ignore
@@ -105,7 +107,7 @@ def to_json(self) -> Dict:
}
def to_key(self) -> Tuple[str, int]:
- return (self.rank_name, self.treedef_id)
+ return (self.rank_name, self.treedef_id) # Check this line in unit test
def check_rank(self, tree: str) -> bool:
return TreeRank(self.rank_name, tree, self.treedef_id).check_rank()
@@ -130,7 +132,7 @@ def to_json(self) -> Dict:
result = {"ranks": {}} # type: ignore
for rank, cols in self.ranks.items():
- rank_key = rank.rank_name if isinstance(rank, TreeRankRecord) else rank
+ rank_key = rank.rank_name if isinstance(rank, TreeRankRecord) else rank # Check this line in unit test
treeNodeCols = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in cols.items()}
if len(cols) == 1:
From 03b678413a0ac332375194592f9f60094907ab42 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Mon, 26 Aug 2024 11:22:40 -0500
Subject: [PATCH 29/78] set treeId dynamically in unit tests
---
specifyweb/workbench/upload/tests/example_plan.py | 2 --
specifyweb/workbench/upload/tests/testschema.py | 10 ++++++++++
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index 0dd9f50fbe2..9d2d99433af 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -46,14 +46,12 @@
'name': 'Species',
'author': 'Species Author',
},
- treeId = 142 # TODO: Set this dynamically
),
'Subspecies': dict(
treeNodeCols = {
'name': 'Subspecies',
'author': 'Subspecies Author',
},
- treeId = 142 # TODO: Set this dynamically
),
}
)}
diff --git a/specifyweb/workbench/upload/tests/testschema.py b/specifyweb/workbench/upload/tests/testschema.py
index 3b9d1117e68..111776a8e93 100644
--- a/specifyweb/workbench/upload/tests/testschema.py
+++ b/specifyweb/workbench/upload/tests/testschema.py
@@ -14,10 +14,19 @@
from .base import UploadTestsBase
from . import example_plan
+def set_plan_treeId():
+ # Set the treeId in example_plan.json dynamically, so that the unit test doesn't depend on the static treeId to always be the same.
+ from specifyweb.specify.models import Taxontreedefitem
+ tree_id = Taxontreedefitem.objects.filter(name='Species').first().treedef_id
+ example_plan_ranks = example_plan.json['uploadable']['uploadTable']['toMany']['determinations'][0]['toOne']['taxon']['treeRecord']['ranks']
+ for rank in ['Species', 'Subspecies']:
+ example_plan_ranks[rank]['treeId'] = tree_id
+
class SchemaTests(UploadTestsBase):
maxDiff = None
def test_schema_parsing(self) -> None:
+ set_plan_treeId()
Draft7Validator.check_schema(schema)
validate(example_plan.json, schema)
plan = parse_plan(self.collection, example_plan.json).apply_scoping(self.collection)
@@ -26,6 +35,7 @@ def test_schema_parsing(self) -> None:
self.assertEqual(repr(plan), repr(self.example_plan))
def test_unparsing(self) -> None:
+ set_plan_treeId()
self.assertEqual(example_plan.json, parse_plan(self.collection, example_plan.json).unparse())
def test_reject_internal_tree_columns(self) -> None:
From 94154e32299e613a672acdb95433ac0d6327e3df Mon Sep 17 00:00:00 2001
From: melton-jason
Date: Wed, 28 Aug 2024 09:46:19 -0500
Subject: [PATCH 30/78] Implement choosing a specific treeDef in mappers
---
.../js_src/lib/components/DataModel/schema.ts | 7 +-
.../components/InitialContext/treeRanks.ts | 13 +-
.../lib/components/WbPlanView/Mapper.tsx | 15 --
.../lib/components/WbPlanView/Wrapped.tsx | 107 ++--------
.../__tests__/mappingHelpers.test.ts | 10 +-
.../lib/components/WbPlanView/helpers.ts | 5 +-
.../components/WbPlanView/mappingHelpers.ts | 37 +++-
.../lib/components/WbPlanView/navigator.ts | 192 +++++++++++-------
.../components/WbPlanView/navigatorSpecs.ts | 5 +
.../WbPlanView/uploadPlanBuilder.ts | 5 +-
.../components/WbPlanView/uploadPlanParser.ts | 1 -
.../frontend/js_src/lib/localization/query.ts | 3 +
.../js_src/lib/localization/workbench.ts | 3 -
13 files changed, 188 insertions(+), 215 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schema.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schema.ts
index f1ed868f49c..a121851d82c 100644
--- a/specifyweb/frontend/js_src/lib/components/DataModel/schema.ts
+++ b/specifyweb/frontend/js_src/lib/components/DataModel/schema.ts
@@ -29,7 +29,8 @@ type Schema = {
'Institution'
];
readonly referenceSymbol: string;
- readonly treeSymbol: string;
+ readonly treeDefinitionSymbol: string;
+ readonly treeRankSymbol: string;
readonly fieldPartSeparator: string;
};
@@ -60,8 +61,10 @@ const schemaBase: Writable = {
// Prefix for -to-many indexes
referenceSymbol: '#',
+ // Prefix for Tree Definitions
+ treeDefinitionSymbol: '%',
// Prefix for tree ranks
- treeSymbol: '$',
+ treeRankSymbol: '$',
// Separator for partial fields (date parts in Query Builder)
fieldPartSeparator: '-',
};
diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/treeRanks.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/treeRanks.ts
index 44c6deeba2b..29c84c9da1c 100644
--- a/specifyweb/frontend/js_src/lib/components/InitialContext/treeRanks.ts
+++ b/specifyweb/frontend/js_src/lib/components/InitialContext/treeRanks.ts
@@ -118,11 +118,14 @@ export function getTreeDefinitions(
tableName
);
- return typeof treeDefinitionId === 'number'
- ? specificTreeDefinitions.filter(
- ({ definition }) => definition.id === treeDefinitionId
- )
- : specificTreeDefinitions;
+ if (typeof treeDefinitionId === 'number') {
+ const resolvedDefinition = specificTreeDefinitions.find(
+ ({ definition }) => definition.id === treeDefinitionId
+ );
+ return resolvedDefinition === undefined
+ ? specificTreeDefinitions
+ : [resolvedDefinition];
+ } else return specificTreeDefinitions;
}
export function getTreeDefinitionItems(
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
index 3125f322e3b..044dbca8ab3 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
@@ -24,15 +24,10 @@ import { className } from '../Atoms/className';
import { icons } from '../Atoms/Icons';
import { Link } from '../Atoms/Link';
import { LoadingContext, ReadOnlyContext } from '../Core/Contexts';
-import type {
- FilterTablesByEndsWith,
- SerializedResource,
-} from '../DataModel/helperTypes';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { softFail } from '../Errors/Crash';
import { ErrorBoundary } from '../Errors/ErrorBoundary';
-import type { TreeInformation } from '../InitialContext/treeRanks';
import { TableIcon } from '../Molecules/TableIcon';
import { Layout } from './Header';
import {
@@ -144,11 +139,6 @@ export function Mapper(props: {
readonly changesMade: boolean;
readonly lines: RA;
readonly mustMatchPreferences: IR;
- readonly taxonType?: string;
- readonly treeDefinitions:
- | readonly SerializedResource>[]
- | undefined;
- readonly handleTreeSelection: (treeName: string) => void;
}): JSX.Element {
const [state, dispatch] = React.useReducer(
reducer,
@@ -457,13 +447,9 @@ export function Mapper(props: {
mustMatchPreferences: state.mustMatchPreferences,
generateFieldData: 'all',
spec: navigatorSpecs.wbPlanView,
- taxonType: props.taxonType,
- treeDefinitions: props.treeDefinitions,
}),
customSelectType: 'OPENED_LIST',
onChange({ isDoubleClick, ...rest }) {
- if (rest.index === 0 && rest.newTableName === 'Taxon')
- props.handleTreeSelection(rest.newValue);
if (isDoubleClick && mapButtonEnabled)
dispatch({ type: 'MappingViewMapAction' });
else if (!isReadOnly)
@@ -549,7 +535,6 @@ export function Mapper(props: {
mustMatchPreferences: state.mustMatchPreferences,
generateFieldData: 'all',
spec: navigatorSpecs.wbPlanView,
- taxonType: props.taxonType,
}),
});
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
index 5ffccd52b74..4f6e50edefa 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx
@@ -9,19 +9,11 @@ import { useNavigate } from 'react-router-dom';
import type { LocalizedString } from 'typesafe-i18n';
import type { State } from 'typesafe-reducer';
-import { usePromise } from '../../hooks/useAsyncState';
import { useErrorContext } from '../../hooks/useErrorContext';
import { useLiveState } from '../../hooks/useLiveState';
-import { commonText } from '../../localization/common';
-import { wbText } from '../../localization/workbench';
-import { type IR, type RA, localized } from '../../utils/types';
-import { caseInsensitiveHash } from '../../utils/utils';
-import { Ul } from '../Atoms';
-import { Button } from '../Atoms/Button';
+import type { IR, RA } from '../../utils/types';
import type { Tables } from '../DataModel/types';
-import { treeRanksPromise } from '../InitialContext/treeRanks';
import { useTitle } from '../Molecules/AppTitle';
-import { Dialog } from '../Molecules/Dialog';
import { ProtectedAction } from '../Permissions/PermissionDenied';
import type { UploadResult } from '../WorkBench/resultsParser';
import { savePlan } from './helpers';
@@ -129,76 +121,24 @@ export function WbPlanView({
);
useErrorContext('state', state);
- const [isTaxonTable, setIsTaxonTable] = React.useState(false);
-
- const [treeDefinitions] = usePromise(treeRanksPromise, true);
- const definitionsForTreeTaxon = treeDefinitions
- ? caseInsensitiveHash(treeDefinitions, 'Taxon')
- : undefined;
- const definitions = definitionsForTreeTaxon?.map(
- ({ definition }) => definition
- );
-
- const [taxonType, setTaxonType] = React.useState();
- const handleTreeType = (taxonTreeDefinitionName: string): void => {
- setTaxonType(taxonTreeDefinitionName);
- setState({
- type: 'MappingState',
- changesMade: true,
- baseTableName: 'Taxon',
- lines: getLinesFromHeaders({
- headers,
- runAutoMapper: true,
- baseTableName: 'Taxon',
- }),
- mustMatchPreferences: {},
- });
- };
- const taxonTree = definitionsForTreeTaxon?.find(
- (tree) => tree.definition.name === taxonType
- );
- const taxonTreeId = taxonTree?.definition.id;
- const taxonCorrespondingId = definitionsForTreeTaxon?.find(
- (tree) => tree.definition.id === uploadPlan?.taxonTreeId
- );
- const taxonIdName = taxonCorrespondingId?.definition.name;
-
- const [treeSelected, setTreeSelected] = React.useState();
-
- const handleTreeSelection = (treeName: string): void => {
- const taxonCorresponding = definitionsForTreeTaxon?.find(
- (tree) => tree.definition.name === treeName
- );
- const taxonIdName = taxonCorresponding?.definition.name;
- setTreeSelected(taxonIdName);
- };
-
const navigate = useNavigate();
return state.type === 'SelectBaseTable' ? (
navigate(`/specify/workbench/${dataset.id}/`)}
- onSelected={
- (baseTableName): void => {
- /*
- * If (baseTableName === 'Taxon') {
- * setIsTaxonTable(true);
- * } else {
- */
- setState({
- type: 'MappingState',
- changesMade: true,
+ onSelected={(baseTableName): void =>
+ setState({
+ type: 'MappingState',
+ changesMade: true,
+ baseTableName,
+ lines: getLinesFromHeaders({
+ headers,
+ runAutoMapper: true,
baseTableName,
- lines: getLinesFromHeaders({
- headers,
- runAutoMapper: true,
- baseTableName,
- }),
- mustMatchPreferences: {},
- });
- }
- // }
+ }),
+ mustMatchPreferences: {},
+ })
}
onSelectTemplate={(uploadPlan, headers): void =>
setState({
@@ -208,36 +148,14 @@ export function WbPlanView({
})
}
/>
- {isTaxonTable && (
-
- )}
) : (
setState({
type: 'SelectBaseTable',
@@ -249,7 +167,6 @@ export function WbPlanView({
baseTableName: state.baseTableName,
lines,
mustMatchPreferences,
- taxonTreeId,
}).then(() => navigate(`/specify/workbench/${dataset.id}/`))
}
/>
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/mappingHelpers.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/mappingHelpers.test.ts
index d6df35e6dfb..866f9ec1520 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/mappingHelpers.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/mappingHelpers.test.ts
@@ -16,7 +16,7 @@ theories(valueIsToManyIndex, [
[[`${schema.referenceSymbol}2`], true],
[[`${schema.referenceSymbol}999`], true],
[['collectionobject'], false],
- [[`${schema.treeSymbol}Kingdom`], false],
+ [[`${schema.treeRankSymbol}Kingdom`], false],
]);
theories(valueIsTreeRank, [
@@ -24,8 +24,8 @@ theories(valueIsTreeRank, [
[[`${schema.referenceSymbol}2`], false],
[[`${schema.referenceSymbol}999`], false],
[['collectionobject'], false],
- [[`${schema.treeSymbol}Kingdom`], true],
- [[`${schema.treeSymbol}County`], true],
+ [[`${schema.treeRankSymbol}Kingdom`], true],
+ [[`${schema.treeRankSymbol}County`], true],
]);
theories(getNumberFromToManyIndex, [
@@ -36,8 +36,8 @@ theories(getNumberFromToManyIndex, [
]);
theories(getNameFromTreeRankName, [
- [[`${schema.treeSymbol}Kingdom`], 'Kingdom'],
- [[`${schema.treeSymbol}County`], 'County'],
+ [[`${schema.treeRankSymbol}Kingdom`], 'Kingdom'],
+ [[`${schema.treeRankSymbol}County`], 'County'],
]);
theories(findDuplicateMappings, [
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
index af69d9667e7..7b7dd213d92 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/helpers.ts
@@ -42,13 +42,11 @@ export async function savePlan({
baseTableName,
lines,
mustMatchPreferences,
- taxonTreeId,
}: {
readonly dataset: Dataset;
readonly baseTableName: keyof Tables;
readonly lines: RA;
readonly mustMatchPreferences: IR;
- readonly taxonTreeId?: number | undefined;
}): Promise {
const renamedLines = renameNewlyCreatedHeaders(
baseTableName,
@@ -68,8 +66,7 @@ export async function savePlan({
const uploadPlan = uploadPlanBuilder(
baseTableName,
renamedLines,
- getMustMatchTables({ baseTableName, lines, mustMatchPreferences }),
- taxonTreeId
+ getMustMatchTables({ baseTableName, lines, mustMatchPreferences })
);
const dataSetRequestUrl = `/api/workbench/dataset/${dataset.id}/`;
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
index a5af31c19c9..dc65961182d 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
@@ -29,9 +29,20 @@ export const valueIsToManyIndex = (value: string | undefined): boolean =>
value?.slice(0, schema.referenceSymbol.length) === schema.referenceSymbol ||
false;
+/**
+ * Returns whether a value is any special tree-related meta information
+ * (e.g. tree defintiion, tree rank)
+ */
+export const valueIsTreeMeta = (value: string | undefined): boolean =>
+ valueIsTreeDefinition(value) || valueIsTreeRank(value);
+
+/** Returns whether a value is a tree definition name (e.x %Taxonomy) */
+export const valueIsTreeDefinition = (value: string | undefined): boolean =>
+ value?.startsWith(schema.treeDefinitionSymbol) ?? false;
+
/** Returns whether a value is a tree rank name (e.x $Kingdom, $Order) */
export const valueIsTreeRank = (value: string | undefined): boolean =>
- value?.startsWith(schema.treeSymbol) || false;
+ value?.startsWith(schema.treeRankSymbol) ?? false;
/**
* Returns index from a formatted -to-many index value (e.x #1 => 1)
@@ -40,6 +51,14 @@ export const valueIsTreeRank = (value: string | undefined): boolean =>
export const getNumberFromToManyIndex = (value: string): number =>
Number(value.slice(schema.referenceSymbol.length));
+/**
+ * Returns tree definition name from a complete tree definition name
+ * (e.x %Taxonomy => Taxonomy)
+ * Opposite of formatTreeDefinition
+ */
+export const getNameFromTreeDefinitionName = (value: string): string =>
+ value.slice(schema.treeDefinitionSymbol.length);
+
/*
* BUG: in places where output of this function is displayed to the user,
* make sure to use tree rank title instead of name
@@ -51,7 +70,7 @@ export const getNumberFromToManyIndex = (value: string): number =>
*
*/
export const getNameFromTreeRankName = (value: string): string =>
- value.slice(schema.treeSymbol.length);
+ value.slice(schema.treeRankSymbol.length);
/**
* Returns a formatted -to-many index from an index (e.x 1 => #1)
@@ -61,7 +80,7 @@ export const formatToManyIndex = (index: number): string =>
`${schema.referenceSymbol}${index}`;
// Meta fields
-export const anyTreeRank = `${schema.fieldPartSeparator}any`;
+export const anyTreeRank = `${schema.fieldPartSeparator}any` as const;
export const formattedEntry = `${schema.fieldPartSeparator}formatted`;
/**
@@ -75,6 +94,14 @@ export const formattedEntry = `${schema.fieldPartSeparator}formatted`;
*/
export const emptyMapping = '0';
+/**
+ * Returns a complete tree definition name from a tree definition name
+ * (e.x Taxononomy => %Taxonomy)
+ * Opposite of getNameFromTreeDefinitionName
+ */
+export const formatTreeDefinition = (definitionName: string): string =>
+ `${schema.treeDefinitionSymbol}${definitionName}`;
+
/**
* Returns a complete tree rank name from a tree rank name
* (e.x Kingdom => $Kingdom)
@@ -82,7 +109,7 @@ export const emptyMapping = '0';
*
*/
export const formatTreeRank = (rankName: string): string =>
- `${schema.treeSymbol}${rankName}`;
+ `${schema.treeRankSymbol}${rankName}`;
// Match fields names like startDate_fullDate, but not _formatted
export const valueIsPartialField = (value: string): boolean =>
@@ -160,7 +187,7 @@ export const getGenericMappingPath = (mappingPath: MappingPath): MappingPath =>
mappingPath.filter(
(mappingPathPart) =>
!valueIsToManyIndex(mappingPathPart) &&
- !valueIsTreeRank(mappingPathPart) &&
+ !valueIsTreeMeta(mappingPathPart) &&
mappingPathPart !== formattedEntry &&
mappingPathPart !== emptyMapping
);
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index 341329c0dab..d189273949b 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -10,16 +10,11 @@ import { queryText } from '../../localization/query';
import type { IR, RA, WritableArray } from '../../utils/types';
import { defined, filterArray } from '../../utils/types';
import { dateParts } from '../Atoms/Internationalization';
-import type {
- AnyTree,
- FilterTablesByEndsWith,
- SerializedResource,
-} from '../DataModel/helperTypes';
+import type { AnyTree } from '../DataModel/helperTypes';
import type { Relationship } from '../DataModel/specifyField';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { getFrontEndOnlyFields, strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
-import type { TreeInformation } from '../InitialContext/treeRanks';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { hasTablePermission, hasTreeAccess } from '../Permissions/helpers';
import type { CustomSelectSubtype } from './CustomSelectElement';
@@ -34,13 +29,17 @@ import {
formatPartialField,
formattedEntry,
formatToManyIndex,
+ formatTreeDefinition,
formatTreeRank,
getGenericMappingPath,
+ getNameFromTreeDefinitionName,
getNameFromTreeRankName,
parsePartialField,
relationshipIsToMany,
valueIsPartialField,
valueIsToManyIndex,
+ valueIsTreeDefinition,
+ valueIsTreeMeta,
valueIsTreeRank,
} from './mappingHelpers';
import { getMaxToManyIndex, isCircularRelationship } from './modelHelpers';
@@ -69,9 +68,14 @@ type NavigationCallbacks = {
readonly handleToManyChildren: (
callbackPayload: Readonly
) => void;
+ readonly handleTreeDefinitions: (
+ callbackPayload: Readonly
+ ) => void;
// Handles tree ranks children
readonly handleTreeRanks: (
- callbackPayload: Readonly
+ callbackPayload: Readonly & {
+ readonly definitionName: string;
+ }
) => void;
// Handles field and relationships
readonly handleSimpleFields: (
@@ -117,10 +121,10 @@ function navigator({
const childrenAreToManyElements =
relationshipIsToMany(parentRelationship) &&
!valueIsToManyIndex(parentPartName) &&
- !valueIsTreeRank(parentPartName);
+ !valueIsTreeMeta(parentPartName);
const childrenAreRanks =
- isTreeTable(table.name) && !valueIsTreeRank(parentPartName);
+ isTreeTable(table.name) && !valueIsTreeMeta(parentPartName);
const callbackPayload = {
table,
@@ -129,11 +133,26 @@ function navigator({
if (childrenAreToManyElements)
callbacks.handleToManyChildren(callbackPayload);
- else if (childrenAreRanks) callbacks.handleTreeRanks(callbackPayload);
+ else if (childrenAreRanks) {
+ const definitions = getTreeDefinitions(table.name, 'all');
+
+ if (definitions.length > 1)
+ callbacks.handleTreeDefinitions(callbackPayload);
+ else
+ callbacks.handleTreeRanks({
+ ...callbackPayload,
+ definitionName: definitions[0].definition.name ?? anyTreeRank,
+ });
+ } else if (valueIsTreeDefinition(parentPartName))
+ callbacks.handleTreeRanks({
+ ...callbackPayload,
+ definitionName: getNameFromTreeDefinitionName(parentPartName),
+ });
else callbacks.handleSimpleFields(callbackPayload);
const isSpecial =
- valueIsToManyIndex(next.partName) || valueIsTreeRank(next.partName);
+ valueIsToManyIndex(next.partName) || valueIsTreeMeta(next.partName);
+
const nextField = isSpecial
? parentRelationship
: table.getField(next.fieldName);
@@ -199,8 +218,6 @@ export function getMappingLineData({
showHiddenFields = false,
mustMatchPreferences = {},
spec,
- taxonType,
- treeDefinitions,
}: {
readonly baseTableName: keyof Tables;
// The mapping path
@@ -215,10 +232,6 @@ export function getMappingLineData({
readonly showHiddenFields?: boolean;
readonly mustMatchPreferences?: IR;
readonly spec: NavigatorSpec;
- readonly taxonType?: string;
- readonly treeDefinitions:
- | readonly SerializedResource>[]
- | undefined;
}): RA {
if (
process.env.NODE_ENV !== 'production' &&
@@ -270,24 +283,7 @@ export function getMappingLineData({
customSelectSubtype,
defaultValue: internalState.defaultValue,
selectLabel: table.label,
- fieldsData: Object.fromEntries(
- filterArray(fieldsData)
- .filter((field) => {
- if (table.name === 'Taxon' && mappingPath[0] === '0') {
- return treeDefinitions?.map((definition) => definition.name);
- }
- /*
- * Else if (
- * internalState.mappingLineData.length === 0 &&
- * taxonType !== undefined
- * ) {
- * return field[1].tableTreeDefName === taxonType;
- * }
- */
- return true;
- })
- .map(([rawKey, fieldData]) => [rawKey, fieldData])
- ),
+ fieldsData: Object.fromEntries(filterArray(fieldsData)),
tableName: table.name,
});
@@ -313,7 +309,7 @@ export function getMappingLineData({
return {
partName: nextPart,
fieldName:
- valueIsTreeRank(nextPart) || valueIsToManyIndex(nextPart)
+ valueIsTreeMeta(nextPart) || valueIsToManyIndex(nextPart)
? valueIsToManyIndex(mappingPath[internalState.position - 1])
? mappingPath[internalState.position - 2]
: mappingPath[internalState.position - 1]
@@ -361,27 +357,67 @@ export function getMappingLineData({
);
},
- handleTreeRanks({ table }) {
+ handleTreeDefinitions({ table }) {
+ const defaultValue = getNameFromTreeDefinitionName(
+ internalState.defaultValue
+ );
+
+ commitInstanceData(
+ 'tree',
+ table,
+ generateFieldData === 'none' ||
+ !hasTreeAccess(table.name as AnyTree['tableName'], 'read')
+ ? []
+ : [
+ spec.includeAnyTreeDefinition &&
+ (generateFieldData === 'all' ||
+ internalState.defaultValue ===
+ formatTreeDefinition(anyTreeRank))
+ ? [
+ formatTreeDefinition(anyTreeRank),
+ {
+ optionLabel: queryText.anyTree(),
+ isRelationship: true,
+ isDefault:
+ internalState.defaultValue ===
+ formatTreeDefinition(anyTreeRank),
+ isEnabled: true,
+ tableName: table.name,
+ },
+ ]
+ : undefined,
+ ...(spec.includeSpecificTreeRanks
+ ? getTreeDefinitions(
+ table.name as AnyTree['tableName'],
+ 'all'
+ ).map(({ definition: { name } }) =>
+ name === defaultValue || generateFieldData === 'all'
+ ? ([
+ formatTreeDefinition(name),
+ {
+ optionLabel: name,
+ isRelationship: true,
+ isDefault: name === defaultValue,
+ tableName: table.name,
+ tableTreeDefName: name,
+ },
+ ] as const)
+ : undefined
+ )
+ : []),
+ ]
+ );
+ },
+
+ handleTreeRanks({ table, definitionName }) {
const defaultValue = getNameFromTreeRankName(internalState.defaultValue);
- const fieldsTreeData: readonly (
- | readonly [string, HtmlGeneratorFieldData]
- | undefined
- )[] =
+ commitInstanceData(
+ 'tree',
+ table,
generateFieldData === 'none' ||
- !hasTreeAccess(table.name as 'Geography', 'read')
+ !hasTreeAccess(table.name as AnyTree['tableName'], 'read')
? []
- : table.name === 'Taxon' && mappingPath[0] === '0' && treeDefinitions
- ? treeDefinitions.map((treeDefinition) => [
- treeDefinition.name,
- {
- optionLabel: treeDefinition.name,
- isRelationship: true,
- isDefault: false,
- tableName: 'Taxon',
- tableTreeDefName: treeDefinition.name,
- },
- ])
: [
spec.includeAnyTreeRank &&
(generateFieldData === 'all' ||
@@ -400,30 +436,32 @@ export function getMappingLineData({
]
: undefined,
...(spec.includeSpecificTreeRanks
- ? getTreeDefinitions(
- table.name as AnyTree['tableName'],
- 'all'
- ).flatMap(({ definition, ranks }) =>
- // Exclude the root rank for each tree
- ranks.slice(1).map(({ name, title }) =>
- name === defaultValue || generateFieldData === 'all'
- ? ([
- formatTreeRank(name),
- {
- optionLabel: title ?? name,
- isRelationship: true,
- isDefault: name === defaultValue,
- tableName: table.name,
- tableTreeDefName: definition.name,
- },
- ] as const)
- : undefined
+ ? getTreeDefinitions(table.name as AnyTree['tableName'], 'all')
+ .filter(
+ ({ definition: { name } }) =>
+ definitionName === name ||
+ definitionName === anyTreeRank
+ )
+ .flatMap(({ definition, ranks }) =>
+ // Exclude the root rank for each tree
+ ranks.slice(1).map(({ name, title }) =>
+ name === defaultValue || generateFieldData === 'all'
+ ? ([
+ formatTreeRank(name),
+ {
+ optionLabel: title ?? name,
+ isRelationship: true,
+ isDefault: name === defaultValue,
+ tableName: table.name,
+ tableTreeDefName: definition.name,
+ },
+ ] as const)
+ : undefined
+ )
)
- )
: []),
- ];
-
- commitInstanceData('tree', table, fieldsTreeData);
+ ]
+ );
},
handleSimpleFields({ table, parentRelationship }) {
@@ -636,7 +674,9 @@ export function getMappingLineData({
: internalState.mappingLineData.filter(
({ customSelectSubtype }) => customSelectSubtype !== 'toMany'
);
- return spec.includeAnyTreeRank || spec.includeSpecificTreeRanks
+ return spec.includeAnyTreeRank ||
+ spec.includeAnyTreeDefinition ||
+ spec.includeSpecificTreeRanks
? filtered
: filtered.filter(
({ customSelectSubtype }) => customSelectSubtype !== 'tree'
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
index 643a974ab0d..2aaa30f6457 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
@@ -14,6 +14,7 @@ export type NavigatorSpec = {
// Whether no restrictions mode is enabled
readonly isNoRestrictions: () => boolean;
readonly includeToManyReferenceNumbers: boolean;
+ readonly includeAnyTreeDefinition: boolean;
readonly includeSpecificTreeRanks: boolean;
readonly includeAnyTreeRank: boolean;
readonly includeFormattedAggregated: boolean;
@@ -41,6 +42,7 @@ const wbPlanView: NavigatorSpec = {
userPreferences.get('workBench', 'wbPlanView', 'noRestrictionsMode'),
includeToManyReferenceNumbers: true,
includeSpecificTreeRanks: true,
+ includeAnyTreeDefinition: false,
includeAnyTreeRank: false,
includeFormattedAggregated: false,
includeRootFormattedAggregated: false,
@@ -81,6 +83,7 @@ const queryBuilder: NavigatorSpec = {
isNoRestrictions: () =>
userPreferences.get('queryBuilder', 'general', 'noRestrictionsMode'),
includeToManyReferenceNumbers: false,
+ includeAnyTreeDefinition: true,
includeSpecificTreeRanks: true,
includeAnyTreeRank: true,
includeFormattedAggregated: true,
@@ -111,6 +114,7 @@ const formatterEditor: NavigatorSpec = {
includeReadOnly: true,
isNoRestrictions: () => true,
includeToManyReferenceNumbers: false,
+ includeAnyTreeDefinition: false,
includeSpecificTreeRanks: false,
includeAnyTreeRank: false,
includeFormattedAggregated: true,
@@ -133,6 +137,7 @@ const permissive: NavigatorSpec = {
includeReadOnly: true,
isNoRestrictions: () => true,
includeToManyReferenceNumbers: true,
+ includeAnyTreeDefinition: true,
includeSpecificTreeRanks: true,
includeAnyTreeRank: true,
includeFormattedAggregated: true,
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index ede50bf503d..1d820221071 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -38,7 +38,6 @@ const toTreeRecordRanks = (
])
);
-// TODO: need to update this to include tree def identifier
const toTreeRecordVariety = (lines: RA): TreeRecord => ({
ranks: Object.fromEntries(
indexMappings(lines).map(([fullRankName, rankMappedFields]) => [
@@ -138,8 +137,7 @@ const toUploadable = (
export const uploadPlanBuilder = (
baseTableName: keyof Tables,
lines: RA,
- mustMatchPreferences: RR,
- taxonTreeId: number | undefined
+ mustMatchPreferences: RR
): UploadPlan => ({
baseTableName: toLowerCase(baseTableName),
uploadable: toUploadable(
@@ -150,7 +148,6 @@ export const uploadPlanBuilder = (
.map(([tableName]) => tableName),
true
),
- taxonTreeId,
});
const indexMappings = (
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 4fc801073c7..3a9463ca621 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -46,7 +46,6 @@ export type Uploadable = TreeRecordVariety | UploadTableVariety;
export type UploadPlan = {
readonly baseTableName: Lowercase;
readonly uploadable: Uploadable;
- readonly taxonTreeId: number | undefined;
};
const parseColumnOptions = (matchingOptions: ColumnOptions): ColumnOptions => ({
diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts
index 3e6785b534f..dc727eb393e 100644
--- a/specifyweb/frontend/js_src/lib/localization/query.ts
+++ b/specifyweb/frontend/js_src/lib/localization/query.ts
@@ -343,6 +343,9 @@ export const queryText = createDictionary({
'uk-ua': '(будь-який ранг)',
'de-ch': '(jeder Rang)',
},
+ anyTree: {
+ 'en-us': '(any tree)',
+ },
moveUp: {
comment: 'As in move it up',
'en-us': 'Move Up',
diff --git a/specifyweb/frontend/js_src/lib/localization/workbench.ts b/specifyweb/frontend/js_src/lib/localization/workbench.ts
index 7fd6148ad5e..133aaac36c6 100644
--- a/specifyweb/frontend/js_src/lib/localization/workbench.ts
+++ b/specifyweb/frontend/js_src/lib/localization/workbench.ts
@@ -1584,7 +1584,4 @@ export const wbText = createDictionary({
'fr-fr': '{node:string} (dans {parent:string})',
'uk-ua': '{node:string} (у {parent:string})',
},
- selectTree: {
- 'en-us': 'Select Tree Type',
- },
} as const);
From 4f657efcb6011b64c5ca9615c1913dc012776ab9 Mon Sep 17 00:00:00 2001
From: melton-jason
Date: Wed, 28 Aug 2024 10:00:51 -0500
Subject: [PATCH 31/78] Disable specific tree interface for all components save
WB
---
.../components/WbPlanView/LineComponents.tsx | 1 -
.../WbPlanView/__tests__/navigator.test.ts | 20 -------------------
.../lib/components/WbPlanView/navigator.ts | 15 ++++++++------
.../components/WbPlanView/navigatorSpecs.ts | 7 +++++++
4 files changed, 16 insertions(+), 27 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx
index df5aceaf43f..433a4761b77 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/LineComponents.tsx
@@ -37,7 +37,6 @@ export type HtmlGeneratorFieldData = {
readonly isDefault?: boolean;
readonly isRelationship?: boolean;
readonly tableName?: keyof Tables;
- readonly tableTreeDefName?: string;
};
type MappingLineBaseProps = {
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/navigator.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/navigator.test.ts
index 7ebc8389786..cec41085ea3 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/navigator.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/navigator.test.ts
@@ -314,70 +314,60 @@ theories(getMappingLineData, [
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Phylum: {
optionLabel: 'Phylum',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Class: {
optionLabel: 'Class',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Order: {
optionLabel: 'Order',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Family: {
optionLabel: 'Family',
isRelationship: true,
isDefault: true,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subfamily: {
optionLabel: 'Subfamily',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Genus: {
optionLabel: 'Genus',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subgenus: {
optionLabel: 'Subgenus',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Species: {
optionLabel: 'Species',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subspecies: {
optionLabel: 'Subspecies',
isRelationship: true,
isDefault: false,
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
},
tableName: 'Taxon',
@@ -870,70 +860,60 @@ theories(getMappingLineData, [
isRelationship: true,
optionLabel: 'Class',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Family: {
isDefault: true,
isRelationship: true,
optionLabel: 'Family',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Genus: {
isDefault: false,
isRelationship: true,
optionLabel: 'Genus',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Kingdom: {
isDefault: false,
isRelationship: true,
optionLabel: 'Kingdom',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Order: {
isDefault: false,
isRelationship: true,
optionLabel: 'Order',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Phylum: {
isDefault: false,
isRelationship: true,
optionLabel: 'Phylum',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Species: {
isDefault: false,
isRelationship: true,
optionLabel: 'Species',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subfamily: {
isDefault: false,
isRelationship: true,
optionLabel: 'Subfamily',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subgenus: {
isDefault: false,
isRelationship: true,
optionLabel: 'Subgenus',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
$Subspecies: {
isDefault: false,
isRelationship: true,
optionLabel: 'Subspecies',
tableName: 'Taxon',
- tableTreeDefName: 'Taxonomy',
},
},
selectLabel: localized('Taxon'),
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
index d189273949b..d551d1e996e 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigator.ts
@@ -40,7 +40,6 @@ import {
valueIsToManyIndex,
valueIsTreeDefinition,
valueIsTreeMeta,
- valueIsTreeRank,
} from './mappingHelpers';
import { getMaxToManyIndex, isCircularRelationship } from './modelHelpers';
import type { NavigatorSpec } from './navigatorSpecs';
@@ -93,10 +92,12 @@ type NavigationCallbacks = {
* fields from multiple relationships at once)
*/
function navigator({
+ spec,
callbacks,
recursivePayload,
baseTableName,
}: {
+ readonly spec: NavigatorSpec;
// Callbacks can be modified depending on the need to make navigator versatile
readonly callbacks: NavigationCallbacks;
// Used internally to make navigator call itself multiple times
@@ -136,12 +137,14 @@ function navigator({
else if (childrenAreRanks) {
const definitions = getTreeDefinitions(table.name, 'all');
- if (definitions.length > 1)
+ if (definitions.length > 1 && spec.useSpecificTreeInterface)
callbacks.handleTreeDefinitions(callbackPayload);
else
callbacks.handleTreeRanks({
...callbackPayload,
- definitionName: definitions[0].definition.name ?? anyTreeRank,
+ definitionName: spec.useSpecificTreeInterface
+ ? definitions[0].definition.name ?? anyTreeRank
+ : anyTreeRank,
});
} else if (valueIsTreeDefinition(parentPartName))
callbacks.handleTreeRanks({
@@ -165,6 +168,7 @@ function navigator({
if (typeof nextTable === 'object' && nextField?.isRelationship !== false)
navigator({
+ spec,
callbacks,
recursivePayload: {
table: nextTable,
@@ -399,7 +403,6 @@ export function getMappingLineData({
isRelationship: true,
isDefault: name === defaultValue,
tableName: table.name,
- tableTreeDefName: name,
},
] as const)
: undefined
@@ -442,7 +445,7 @@ export function getMappingLineData({
definitionName === name ||
definitionName === anyTreeRank
)
- .flatMap(({ definition, ranks }) =>
+ .flatMap(({ ranks }) =>
// Exclude the root rank for each tree
ranks.slice(1).map(({ name, title }) =>
name === defaultValue || generateFieldData === 'all'
@@ -453,7 +456,6 @@ export function getMappingLineData({
isRelationship: true,
isDefault: name === defaultValue,
tableName: table.name,
- tableTreeDefName: definition.name,
},
] as const)
: undefined
@@ -665,6 +667,7 @@ export function getMappingLineData({
};
navigator({
+ spec,
callbacks,
baseTableName,
});
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
index 2aaa30f6457..3619bb9e853 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/navigatorSpecs.ts
@@ -14,6 +14,8 @@ export type NavigatorSpec = {
// Whether no restrictions mode is enabled
readonly isNoRestrictions: () => boolean;
readonly includeToManyReferenceNumbers: boolean;
+ // REFACTOR: remove this attribute
+ readonly useSpecificTreeInterface: boolean;
readonly includeAnyTreeDefinition: boolean;
readonly includeSpecificTreeRanks: boolean;
readonly includeAnyTreeRank: boolean;
@@ -41,6 +43,7 @@ const wbPlanView: NavigatorSpec = {
isNoRestrictions: () =>
userPreferences.get('workBench', 'wbPlanView', 'noRestrictionsMode'),
includeToManyReferenceNumbers: true,
+ useSpecificTreeInterface: true,
includeSpecificTreeRanks: true,
includeAnyTreeDefinition: false,
includeAnyTreeRank: false,
@@ -83,6 +86,8 @@ const queryBuilder: NavigatorSpec = {
isNoRestrictions: () =>
userPreferences.get('queryBuilder', 'general', 'noRestrictionsMode'),
includeToManyReferenceNumbers: false,
+ // REFACTOR: see https://github.com/specify/specify7/pull/5251
+ useSpecificTreeInterface: false,
includeAnyTreeDefinition: true,
includeSpecificTreeRanks: true,
includeAnyTreeRank: true,
@@ -114,6 +119,7 @@ const formatterEditor: NavigatorSpec = {
includeReadOnly: true,
isNoRestrictions: () => true,
includeToManyReferenceNumbers: false,
+ useSpecificTreeInterface: false,
includeAnyTreeDefinition: false,
includeSpecificTreeRanks: false,
includeAnyTreeRank: false,
@@ -137,6 +143,7 @@ const permissive: NavigatorSpec = {
includeReadOnly: true,
isNoRestrictions: () => true,
includeToManyReferenceNumbers: true,
+ useSpecificTreeInterface: true,
includeAnyTreeDefinition: true,
includeSpecificTreeRanks: true,
includeAnyTreeRank: true,
From 5ad335851f40aaf74c63232609310705be77eb06 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Wed, 28 Aug 2024 14:57:55 -0400
Subject: [PATCH 32/78] Pass tree definition id in API request
---
.../WbPlanView/uploadPlanBuilder.ts | 57 ++++++++++++++-----
1 file changed, 44 insertions(+), 13 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 1d820221071..db08f892c89 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -3,9 +3,14 @@ import { group, removeKey, split, toLowerCase } from '../../utils/utils';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
-import { isTreeTable } from '../InitialContext/treeRanks';
+import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
-import type { SplitMappingPath } from './mappingHelpers';
+import {
+ getNameFromTreeDefinitionName,
+ SplitMappingPath,
+ valueIsTreeDefinition,
+ valueIsTreeRank,
+} from './mappingHelpers';
import { getNameFromTreeRankName, valueIsToManyIndex } from './mappingHelpers';
import type {
ColumnDefinition,
@@ -33,21 +38,47 @@ const toTreeRecordRanks = (
): IR =>
Object.fromEntries(
rankMappedFields.map(({ mappingPath, headerName, columnOptions }) => [
- mappingPath[0].toLowerCase(),
+ // Use the last element of the mapping path to get the field name
+ // e.g: mappingPath when a tree is specified: ['$Kingdom', 'name'] vs when no tree is specified: ['name']
+ mappingPath[mappingPath.length - 1].toLowerCase(),
toColumnOptions(headerName, columnOptions),
])
);
-const toTreeRecordVariety = (lines: RA): TreeRecord => ({
- ranks: Object.fromEntries(
- indexMappings(lines).map(([fullRankName, rankMappedFields]) => [
- getNameFromTreeRankName(fullRankName),
- {
- treeNodeCols: toTreeRecordRanks(rankMappedFields),
- },
- ])
- ),
-});
+const toTreeRecordVariety = (lines: RA): TreeRecord => {
+ const treeDefinitions = getTreeDefinitions('Taxon', 'all');
+
+ return {
+ ranks: Object.fromEntries(
+ indexMappings(lines).map(([fullName, rankMappedFields]) => {
+ const rankName = valueIsTreeRank(fullName)
+ ? getNameFromTreeRankName(fullName)
+ : getNameFromTreeRankName(rankMappedFields[0].mappingPath[0]);
+
+ const treeName = valueIsTreeDefinition(fullName)
+ ? getNameFromTreeDefinitionName(fullName)
+ : undefined;
+
+ const treeId =
+ treeName !== undefined
+ ? treeDefinitions.find(
+ ({ definition }) => definition.name === treeName
+ )?.definition.id
+ : treeDefinitions.find(({ ranks }) =>
+ ranks.find((r) => r.name === rankName)
+ )?.definition.id;
+
+ return [
+ rankName,
+ {
+ treeNodeCols: toTreeRecordRanks(rankMappedFields),
+ treeId,
+ },
+ ];
+ })
+ ),
+ };
+};
function toUploadTable(
table: SpecifyTable,
From 004dda41491f96c45f81cdc5f78aa4fc0d102ea7 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Wed, 28 Aug 2024 15:13:51 -0400
Subject: [PATCH 33/78] Fix tests
---
.../DataModel/__tests__/schemaBase.test.ts | 3 ++-
.../components/Syncer/__tests__/index.test.ts | 4 ++--
.../lib/tests/fixtures/uploadplan.1.json | 18 ++++++++++++------
3 files changed, 16 insertions(+), 9 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
index 8e16373ae35..3aea215586a 100644
--- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
@@ -24,5 +24,6 @@ test('domain data is fetched and parsed correctly', async () =>
],
paleoContextChildTable: 'collectionobject',
referenceSymbol: '#',
- treeSymbol: '$',
+ treeRankSymbol: '$',
+ treeDefinitionSymbol: '%'
}));
diff --git a/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts b/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts
index 89b362c28c0..bb7abf22a9c 100644
--- a/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/Syncer/__tests__/index.test.ts
@@ -94,8 +94,8 @@ test('Editing Data Object Formatter', () => {
-
-
+
+
"
`);
diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
index f0cc682997a..a639bc1327d 100644
--- a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
+++ b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
@@ -283,34 +283,40 @@
"default": null,
"column": "Class"
}
- }
+ },
+ "treeId": 1
},
"Family": {
"treeNodeCols": {
"name": "Family"
- }
+ },
+ "treeId": 1
},
"Genus": {
"treeNodeCols": {
"name": "Genus"
- }
+ },
+ "treeId": 1
},
"Subgenus": {
"treeNodeCols": {
"name": "Subgenus"
- }
+ },
+ "treeId": 1
},
"Species": {
"treeNodeCols": {
"name": "Species",
"author": "Species Author"
- }
+ },
+ "treeId": 1
},
"Subspecies": {
"treeNodeCols": {
"name": "Subspecies",
"author": "Subspecies Author"
- }
+ },
+ "treeId": 1
}
}
}
From 47c9fdf88bd364888300d646981b2ed1f6314ff2 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Wed, 28 Aug 2024 19:17:37 +0000
Subject: [PATCH 34/78] Lint code with ESLint and Prettier
Triggered by 004dda41491f96c45f81cdc5f78aa4fc0d102ea7 on branch refs/heads/issue-4980
---
.../DataModel/__tests__/schemaBase.test.ts | 2 +-
.../WbPlanView/uploadPlanBuilder.ts | 20 ++++++++++---------
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
index 3aea215586a..78524afc219 100644
--- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/schemaBase.test.ts
@@ -25,5 +25,5 @@ test('domain data is fetched and parsed correctly', async () =>
paleoContextChildTable: 'collectionobject',
referenceSymbol: '#',
treeRankSymbol: '$',
- treeDefinitionSymbol: '%'
+ treeDefinitionSymbol: '%',
}));
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index db08f892c89..27b17cd1240 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -5,9 +5,9 @@ import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
+import type { SplitMappingPath } from './mappingHelpers';
import {
getNameFromTreeDefinitionName,
- SplitMappingPath,
valueIsTreeDefinition,
valueIsTreeRank,
} from './mappingHelpers';
@@ -38,9 +38,11 @@ const toTreeRecordRanks = (
): IR =>
Object.fromEntries(
rankMappedFields.map(({ mappingPath, headerName, columnOptions }) => [
- // Use the last element of the mapping path to get the field name
- // e.g: mappingPath when a tree is specified: ['$Kingdom', 'name'] vs when no tree is specified: ['name']
- mappingPath[mappingPath.length - 1].toLowerCase(),
+ /*
+ * Use the last element of the mapping path to get the field name
+ * e.g: mappingPath when a tree is specified: ['$Kingdom', 'name'] vs when no tree is specified: ['name']
+ */
+ mappingPath.at(-1).toLowerCase(),
toColumnOptions(headerName, columnOptions),
])
);
@@ -60,12 +62,12 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
: undefined;
const treeId =
- treeName !== undefined
- ? treeDefinitions.find(
- ({ definition }) => definition.name === treeName
- )?.definition.id
- : treeDefinitions.find(({ ranks }) =>
+ treeName === undefined
+ ? treeDefinitions.find(({ ranks }) =>
ranks.find((r) => r.name === rankName)
+ )?.definition.id
+ : treeDefinitions.find(
+ ({ definition }) => definition.name === treeName
)?.definition.id;
return [
From 790bacab19884415315ce780c2c3b16b8b7739b4 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 29 Aug 2024 10:15:35 -0500
Subject: [PATCH 35/78] TreeRank NamedTuple
---
specifyweb/specify/tree_utils.py | 2 +
specifyweb/workbench/upload/scoping.py | 2 +-
specifyweb/workbench/upload/treerecord.py | 99 +++++++++----------
.../workbench/upload/upload_plan_schema.py | 2 +-
4 files changed, 52 insertions(+), 53 deletions(-)
diff --git a/specifyweb/specify/tree_utils.py b/specifyweb/specify/tree_utils.py
index af442bd6787..ac9cd0783a0 100644
--- a/specifyweb/specify/tree_utils.py
+++ b/specifyweb/specify/tree_utils.py
@@ -5,6 +5,8 @@
lookup = lambda tree: (tree.lower() + 'treedef')
+SPECIFY_TREES = {"taxon", "storage", "geography", "geologictimeperiod", "lithostrat"}
+
def get_search_filters(collection: spmodels.Collection, tree: str):
tree_name = tree.lower()
if tree_name == 'storage':
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 848977c3b9a..9e1f9338491 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -222,7 +222,7 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
scoped_ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]] = {}
for r, cols in tr.ranks.items():
if isinstance(r, str):
- r = TreeRank(r, table.name, treedef.id if treedef else None, tr.base_treedef_id).tree_rank_record()
+ r = TreeRank.create(r, table.name, treedef.id if treedef else None, tr.base_treedef_id).tree_rank_record()
scoped_ranks[r] = {}
for f, colopts in cols.items():
scoped_ranks[r][f] = extend_columnoptions(colopts, collection, table.name, f)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index bd257e3e84e..c3e3c5b2d0f 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -10,6 +10,7 @@
from specifyweb.businessrules.exceptions import BusinessRuleException
from specifyweb.specify import models
+from specifyweb.specify.tree_utils import SPECIFY_TREES
from .column_options import ColumnOptions, ExtendedColumnOptions
from .parsing import ParseResult, WorkBenchParseFailure, parse_many, filter_and_upload
from .upload_result import UploadResult, NullRecord, NoMatch, Matched, \
@@ -19,62 +20,61 @@
logger = logging.getLogger(__name__)
-class TreeRank:
+class TreeRank(NamedTuple):
rank_name: str
treedef_id: int
tree: str
- def __init__(
- self,
+ @staticmethod
+ def create(
rank_name: str,
tree: str,
treedef_id: Optional[int] = None,
base_treedef_id: Optional[int] = None,
- ):
- self.rank_name = rank_name
- self.treedef_id = self._get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
- self.tree = tree.lower()
-
- def _get_treedef_id(
- self,
- rank_name: str,
- tree: str,
- treedef_id: Optional[int],
- base_treedef_id: Optional[int],
- ) -> int:
+ ) -> 'TreeRank':
tree_model = get_tree_model(tree)
-
- filter_kwargs = self._build_filter_kwargs(rank_name, treedef_id)
- treedefitems = tree_model.objects.filter(**filter_kwargs)
-
- if not treedefitems.exists() and treedef_id is not None:
- filter_kwargs = self._build_filter_kwargs(rank_name)
+ tree_lower = tree.lower()
+
+ def _build_filter_kwargs(rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
+ filter_kwargs = {'name': rank_name}
+ if treedef_id is not None:
+ filter_kwargs['treedef_id'] = treedef_id # type: ignore
+ return filter_kwargs
+
+ def _filter_by_base_treedef_id(treedefitems, rank_name: str, base_treedef_id: Optional[int]):
+ if base_treedef_id is None:
+ raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
+ treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
+ if not treedefitems.exists():
+ raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+ if treedefitems.count() > 1:
+ raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
+ return treedefitems
+
+ def _get_treedef_id(
+ rank_name: str,
+ tree: str,
+ treedef_id: Optional[int],
+ base_treedef_id: Optional[int],
+ ) -> int:
+ filter_kwargs = _build_filter_kwargs(rank_name, treedef_id)
treedefitems = tree_model.objects.filter(**filter_kwargs)
+
+ if not treedefitems.exists() and treedef_id is not None:
+ filter_kwargs = _build_filter_kwargs(rank_name)
+ treedefitems = tree_model.objects.filter(**filter_kwargs)
- if treedefitems.count() > 1:
- treedefitems = self._filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
-
- first_item = treedefitems.first()
- if first_item is None:
- raise ValueError(f"No treedefitems found for rank {rank_name}")
+ if treedefitems.count() > 1:
+ treedefitems = _filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
- return first_item.treedef_id
+ first_item = treedefitems.first()
+ if first_item is None:
+ raise ValueError(f"No treedefitems found for rank {rank_name}")
- def _build_filter_kwargs(self, rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
- filter_kwargs = {'name': rank_name}
- if treedef_id is not None:
- filter_kwargs['treedef_id'] = treedef_id # type: ignore
- return filter_kwargs
+ return first_item.treedef_id
- def _filter_by_base_treedef_id(self, treedefitems, rank_name: str, base_treedef_id: Optional[int]):
- if base_treedef_id is None:
- raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
- treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
- if not treedefitems.exists():
- raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
- if treedefitems.count() > 1:
- raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
- return treedefitems
+ treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+ return TreeRank(rank_name, treedef_id, tree_lower)
def check_rank(self) -> bool:
tree_model = get_tree_model(self.tree)
@@ -88,9 +88,7 @@ def validate_rank(self) -> None:
def tree_rank_record(self) -> 'TreeRankRecord':
assert self.rank_name is not None, "Rank name is required"
assert self.treedef_id is not None, "Treedef ID is required"
- assert self.tree is not None and self.tree.lower() in {
- "taxon", "storage", "geography", "geologictimeperiod", "lithostrat" # TODO: Replace with constants
- }, "Tree is required"
+ assert self.tree is not None and self.tree.lower() in SPECIFY_TREES, "Tree is required"
return TreeRankRecord(self.rank_name, self.treedef_id)
def get_tree_model(tree: str):
@@ -107,13 +105,13 @@ def to_json(self) -> Dict:
}
def to_key(self) -> Tuple[str, int]:
- return (self.rank_name, self.treedef_id) # Check this line in unit test
+ return (self.rank_name, self.treedef_id)
def check_rank(self, tree: str) -> bool:
- return TreeRank(self.rank_name, tree, self.treedef_id).check_rank()
+ return TreeRank.create(self.rank_name, tree, self.treedef_id).check_rank()
def validate_rank(self, tree) -> None:
- TreeRank(self.rank_name, tree, self.treedef_id).validate_rank()
+ TreeRank.create(self.rank_name, tree, self.treedef_id).validate_rank()
class TreeRecord(NamedTuple):
name: str
@@ -128,11 +126,10 @@ def get_cols(self) -> Set[str]:
return {col.column for r in self.ranks.values() for col in r.values() if hasattr(col, 'column')}
def to_json(self) -> Dict:
- # result: Dict[str, Union[str, int, Dict[str, Any]]] = {"ranks": {}}
result = {"ranks": {}} # type: ignore
for rank, cols in self.ranks.items():
- rank_key = rank.rank_name if isinstance(rank, TreeRankRecord) else rank # Check this line in unit test
+ rank_key = rank.rank_name if isinstance(rank, TreeRankRecord) else rank
treeNodeCols = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in cols.items()}
if len(cols) == 1:
@@ -167,7 +164,7 @@ def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: A
parseFails: List[WorkBenchParseFailure] = []
for tree_rank_record, cols in self.ranks.items():
nameColumn = cols['name']
- presults, pfails = parse_many(collection, self.name, cols, row) # TODO: fix
+ presults, pfails = parse_many(collection, self.name, cols, row)
parsedFields[tree_rank_record.rank_name] = presults
parseFails += pfails
filters = {k: v for result in presults for k, v in result.filter_on.items()}
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 3c2f4171038..645e3eb71d7 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -320,7 +320,7 @@ def parse_rank(name_or_cols):
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
for rank, name_or_cols in to_parse['ranks'].items():
rank_name, parsed_cols, treedefid = parse_rank(name_or_cols)
- tree_rank_record = TreeRank(rank, table.name, treedefid, base_treedefid).tree_rank_record()
+ tree_rank_record = TreeRank.create(rank, table.name, treedefid, base_treedefid).tree_rank_record()
ranks[tree_rank_record] = parsed_cols
# Update the schema with the treeId
From ca89ba2cf46fe4afd7cd7d55ee9dd34ba677b9cc Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Fri, 30 Aug 2024 13:37:53 -0400
Subject: [PATCH 36/78] Parse treeId in upload plan when creating lines
---
.../components/WbPlanView/uploadPlanParser.ts | 21 ++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 3a9463ca621..30924fa7756 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -3,9 +3,10 @@ import type { SpecifyTable } from '../DataModel/specifyTable';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { softFail } from '../Errors/Crash';
+import { getTreeDefinitions } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { MappingPath } from './Mapper';
-import type { SplitMappingPath } from './mappingHelpers';
+import { formatTreeDefinition, SplitMappingPath } from './mappingHelpers';
import { formatToManyIndex, formatTreeRank } from './mappingHelpers';
export type MatchBehaviors = 'ignoreAlways' | 'ignoreNever' | 'ignoreWhenBlank';
@@ -34,7 +35,10 @@ type UploadTableVariety =
| { readonly uploadTable: UploadTable };
export type TreeRecord = {
- readonly ranks: IR }>;
+ readonly ranks: IR<
+ | string
+ | { readonly treeNodeCols: IR; readonly treeId?: number }
+ >;
};
type TreeRecordVariety =
@@ -70,10 +74,21 @@ const parseTree = (
name: rankData,
}
: rankData.treeNodeCols,
- [...mappingPath, formatTreeRank(rankName)]
+ [
+ ...mappingPath,
+ ...(typeof rankData === 'object' && typeof rankData.treeId === 'number'
+ ? [resolveTreeId(rankData.treeId)]
+ : []),
+ formatTreeRank(rankName),
+ ]
)
);
+const resolveTreeId = (id: number): string => {
+ const treeDefinition = getTreeDefinitions('Taxon', id);
+ return formatTreeDefinition(treeDefinition[0].definition.name);
+};
+
function parseTreeTypes(
table: SpecifyTable,
uploadPlan: TreeRecordVariety,
From d4af2d600e1d6f56cf996651bae575ad885ca558 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Fri, 30 Aug 2024 13:37:58 -0400
Subject: [PATCH 37/78] Fix typecheck
---
.../js_src/lib/components/WbPlanView/uploadPlanBuilder.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 27b17cd1240..b839b5719b1 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -42,7 +42,7 @@ const toTreeRecordRanks = (
* Use the last element of the mapping path to get the field name
* e.g: mappingPath when a tree is specified: ['$Kingdom', 'name'] vs when no tree is specified: ['name']
*/
- mappingPath.at(-1).toLowerCase(),
+ mappingPath.at(-1)?.toLowerCase(),
toColumnOptions(headerName, columnOptions),
])
);
From 9c0b163f8cb6b36850a3cea1b0af142acec3f080 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Fri, 30 Aug 2024 14:27:52 -0400
Subject: [PATCH 38/78] Fix treeId parsing for collections with 1 tree
---
.../js_src/lib/components/WbPlanView/uploadPlanParser.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 30924fa7756..15724d9a741 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -76,7 +76,9 @@ const parseTree = (
: rankData.treeNodeCols,
[
...mappingPath,
- ...(typeof rankData === 'object' && typeof rankData.treeId === 'number'
+ ...(typeof rankData === 'object' &&
+ typeof rankData.treeId === 'number' &&
+ getTreeDefinitions('Taxon', 'all').length > 1
? [resolveTreeId(rankData.treeId)]
: []),
formatTreeRank(rankName),
From 4a4d58ad963f95f9db509c67100deb90015d8adf Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Fri, 30 Aug 2024 18:31:30 +0000
Subject: [PATCH 39/78] Lint code with ESLint and Prettier
Triggered by 9c0b163f8cb6b36850a3cea1b0af142acec3f080 on branch refs/heads/issue-4980
---
.../js_src/lib/components/WbPlanView/uploadPlanParser.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 15724d9a741..b1378674013 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -6,7 +6,8 @@ import { softFail } from '../Errors/Crash';
import { getTreeDefinitions } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { MappingPath } from './Mapper';
-import { formatTreeDefinition, SplitMappingPath } from './mappingHelpers';
+import type { SplitMappingPath } from './mappingHelpers';
+import { formatTreeDefinition } from './mappingHelpers';
import { formatToManyIndex, formatTreeRank } from './mappingHelpers';
export type MatchBehaviors = 'ignoreAlways' | 'ignoreNever' | 'ignoreWhenBlank';
From 5ef3bfac65a1377dbc3471a6260bf69c1edf7554 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 12:50:20 -0500
Subject: [PATCH 40/78] add rescope_tree_from_row in wb tree binding
---
specifyweb/workbench/upload/treerecord.py | 56 +++++++++++++++++--
.../workbench/upload/upload_plan_schema.py | 2 +-
2 files changed, 51 insertions(+), 7 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index c3e3c5b2d0f..4db1eb4f051 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -32,7 +32,7 @@ def create(
treedef_id: Optional[int] = None,
base_treedef_id: Optional[int] = None,
) -> 'TreeRank':
- tree_model = get_tree_model(tree)
+ tree_model = get_treedefitem_model(tree)
tree_lower = tree.lower()
def _build_filter_kwargs(rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
@@ -77,7 +77,7 @@ def _get_treedef_id(
return TreeRank(rank_name, treedef_id, tree_lower)
def check_rank(self) -> bool:
- tree_model = get_tree_model(self.tree)
+ tree_model = get_treedefitem_model(self.tree)
rank = tree_model.objects.filter(name=self.rank_name, treedef_id=self.treedef_id)
return rank.exists() and rank.count() == 1
@@ -91,7 +91,7 @@ def tree_rank_record(self) -> 'TreeRankRecord':
assert self.tree is not None and self.tree.lower() in SPECIFY_TREES, "Tree is required"
return TreeRankRecord(self.rank_name, self.treedef_id)
-def get_tree_model(tree: str):
+def get_treedefitem_model(tree: str):
return getattr(models, tree.lower().title() + 'treedefitem')
class TreeRankRecord(NamedTuple):
@@ -158,10 +158,54 @@ def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
def get_treedefs(self) -> Set:
return set([self.treedef])
+
+ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
+ tree_rank_model = get_treedefitem_model(self.name)
+ tree_node_model = getattr(models, self.name.lower().title())
+
+ # Create a mapping of rank names to their treedef IDs
+ ranks = {rank.rank_name: rank.treedef_id for rank in self.ranks.keys()}
+ rank_names = set(ranks.keys())
+
+ # Find non-null rank columns in the row
+ ranks_in_row_not_null = {
+ col
+ for col, value in row.items()
+ if value is not None and value != "" and col in rank_names
+ }
+
+ # Handle cases with multiple or no ranks in the row
+ if len(ranks_in_row_not_null) > 1:
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
+ if not ranks_in_row_not_null:
+ return self, WorkBenchParseFailure('noRanksInRow', {}, '')
+
+ # Get the target rank name and its treedef ID
+ target_rank_name = ranks_in_row_not_null.pop()
+ target_rank_treedef_id = ranks[target_rank_name]
+
+ # Query for the target rank treedef item
+ treedefitem_query = tree_rank_model.objects.filter(name=target_rank_name, treedef_id=target_rank_treedef_id)
+
+ if not treedefitem_query.exists() or treedefitem_query.count() != 1:
+ return self, WorkBenchParseFailure('invalidRank', {'rank': target_rank_name}, target_rank_name)
+
+ # Fetch the required data
+ treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id))
+ root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ target_rank_treedef = treedefitem_query.first().treedef
+
+ # Return the updated ScopedTreeRecord
+ return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: Auditor, cache: Optional[Dict]=None, row_index: Optional[int] = None) -> Union["BoundTreeRecord", ParseFailures]:
parsedFields: Dict[str, List[ParseResult]] = {}
parseFails: List[WorkBenchParseFailure] = []
+
+ rescoped_tree_record, parse_fail = self.rescope_tree_from_row(row)
+ if parse_fail:
+ parseFails.append(parse_fail)
+
for tree_rank_record, cols in self.ranks.items():
nameColumn = cols['name']
presults, pfails = parse_many(collection, self.name, cols, row)
@@ -180,9 +224,9 @@ def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: A
return BoundTreeRecord(
name=self.name,
- treedef=self.treedef,
- treedefitems=self.treedefitems,
- root=self.root,
+ treedef=rescoped_tree_record.treedef,
+ treedefitems=rescoped_tree_record.treedefitems,
+ root=rescoped_tree_record.root,
disambiguation=self.disambiguation,
parsedFields=parsedFields,
uploadingAgentId=uploadingAgentId,
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 645e3eb71d7..a9034b1f57d 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -6,7 +6,7 @@
from .upload_table import DeferredScopeUploadTable, UploadTable, OneToOneTable, MustMatchTable
from .tomany import ToManyRecord
-from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord, get_tree_model
+from .treerecord import TreeRank, TreeRankRecord, TreeRecord, MustMatchTreeRecord
from .uploadable import Uploadable
from .column_options import ColumnOptions
from .scoping import DEFERRED_SCOPING
From e33c9cf6ec2db0ca180ae45a5aa9ae63acfe16c5 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 14:59:04 -0500
Subject: [PATCH 41/78] fix determinations naming bug
---
specifyweb/workbench/upload/treerecord.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 4db1eb4f051..feea950d6c9 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -490,6 +490,8 @@ def _upload(self, to_upload: List[TreeDefItemWithParseResults], matched: Union[M
def _do_insert(self, model, **kwargs):
obj = model(**kwargs)
+ if hasattr(obj, 'fullname') and obj.fullname is None:
+ obj.fullname = obj.name
obj.save(skip_tree_extras=True)
return obj
From 93810b00f3cd7c1b350e14f2075531083792c982 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 15:54:36 -0500
Subject: [PATCH 42/78] fix unit tests
---
specifyweb/workbench/upload/treerecord.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index feea950d6c9..b5ce724101e 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -163,6 +163,10 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
tree_rank_model = get_treedefitem_model(self.name)
tree_node_model = getattr(models, self.name.lower().title())
+ # Do nothing if there is only one tree involved
+ if len(set([tr.treedef_id for tr in self.ranks.keys()])) == 1:
+ return self, None
+
# Create a mapping of rank names to their treedef IDs
ranks = {rank.rank_name: rank.treedef_id for rank in self.ranks.keys()}
rank_names = set(ranks.keys())
From 99afa31f1ec0e2aa632ece06cb26dae0873c32c0 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 17:51:59 -0500
Subject: [PATCH 43/78] better handle noRanksInRow
---
specifyweb/workbench/upload/treerecord.py | 63 +++++++++++++++--------
1 file changed, 41 insertions(+), 22 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index b5ce724101e..e02027abc53 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -160,46 +160,65 @@ def get_treedefs(self) -> Set:
return set([self.treedef])
def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
- tree_rank_model = get_treedefitem_model(self.name)
- tree_node_model = getattr(models, self.name.lower().title())
+ def fetch_tree_node_model(name: str):
+ return getattr(models, name.lower().title())
- # Do nothing if there is only one tree involved
- if len(set([tr.treedef_id for tr in self.ranks.keys()])) == 1:
+ def fetch_treedefitem_model(name: str):
+ return get_treedefitem_model(name)
+
+ def get_ranks_mapping(ranks):
+ return {rank.rank_name: rank.treedef_id for rank in ranks.keys()}
+
+ def find_non_null_ranks_in_row(row, rank_names):
+ return {
+ col
+ for col, value in row.items()
+ if value is not None and value != "" and col in rank_names
+ }
+
+ def handle_no_ranks_in_row():
+ if self.treedef is not None:
+ return self, None
+ else:
+ first_rank = next(iter(self.ranks.keys()))
+ target_rank_treedef_id = first_rank.treedef_id
+ target_rank_treedef = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id))
+ root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
+
+ def handle_multiple_ranks_in_row(ranks_in_row_not_null):
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
+
+ def handle_invalid_rank(target_rank_name):
+ return self, WorkBenchParseFailure('invalidRank', {'rank': target_rank_name}, target_rank_name)
+
+ tree_rank_model = fetch_treedefitem_model(self.name)
+ tree_node_model = fetch_tree_node_model(self.name)
+
+ if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
return self, None
- # Create a mapping of rank names to their treedef IDs
- ranks = {rank.rank_name: rank.treedef_id for rank in self.ranks.keys()}
+ ranks = get_ranks_mapping(self.ranks)
rank_names = set(ranks.keys())
+ ranks_in_row_not_null = find_non_null_ranks_in_row(row, rank_names)
- # Find non-null rank columns in the row
- ranks_in_row_not_null = {
- col
- for col, value in row.items()
- if value is not None and value != "" and col in rank_names
- }
-
- # Handle cases with multiple or no ranks in the row
if len(ranks_in_row_not_null) > 1:
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
+ return handle_multiple_ranks_in_row(ranks_in_row_not_null)
if not ranks_in_row_not_null:
- return self, WorkBenchParseFailure('noRanksInRow', {}, '')
+ return handle_no_ranks_in_row()
- # Get the target rank name and its treedef ID
target_rank_name = ranks_in_row_not_null.pop()
target_rank_treedef_id = ranks[target_rank_name]
-
- # Query for the target rank treedef item
treedefitem_query = tree_rank_model.objects.filter(name=target_rank_name, treedef_id=target_rank_treedef_id)
if not treedefitem_query.exists() or treedefitem_query.count() != 1:
- return self, WorkBenchParseFailure('invalidRank', {'rank': target_rank_name}, target_rank_name)
+ return handle_invalid_rank(target_rank_name)
- # Fetch the required data
treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
target_rank_treedef = treedefitem_query.first().treedef
- # Return the updated ScopedTreeRecord
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: Auditor, cache: Optional[Dict]=None, row_index: Optional[int] = None) -> Union["BoundTreeRecord", ParseFailures]:
From c4ac5a8805fce5bc7831f6af9055eebf8d5ce7ff Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 18:10:30 -0500
Subject: [PATCH 44/78] order by rankid
---
specifyweb/workbench/upload/treerecord.py | 29 ++++++++++++++++++-----
1 file changed, 23 insertions(+), 6 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index e02027abc53..869850cae2f 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -158,7 +158,7 @@ def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
def get_treedefs(self) -> Set:
return set([self.treedef])
-
+
def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
def fetch_tree_node_model(name: str):
return getattr(models, name.lower().title())
@@ -182,8 +182,14 @@ def handle_no_ranks_in_row():
else:
first_rank = next(iter(self.ranks.keys()))
target_rank_treedef_id = first_rank.treedef_id
- target_rank_treedef = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
- treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id))
+ target_rank_treedef = tree_node_model.objects.filter(
+ definition_id=target_rank_treedef_id, parent=None
+ ).first()
+ treedefitems = list(
+ tree_rank_model.objects.filter(
+ treedef_id=target_rank_treedef_id
+ ).order_by("rankid")
+ )
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
@@ -210,18 +216,28 @@ def handle_invalid_rank(target_rank_name):
target_rank_name = ranks_in_row_not_null.pop()
target_rank_treedef_id = ranks[target_rank_name]
- treedefitem_query = tree_rank_model.objects.filter(name=target_rank_name, treedef_id=target_rank_treedef_id)
+ treedefitem_query = tree_rank_model.objects.filter(
+ name=target_rank_name, treedef_id=target_rank_treedef_id
+ ).order_by("rankid")
if not treedefitem_query.exists() or treedefitem_query.count() != 1:
return handle_invalid_rank(target_rank_name)
- treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id))
+ treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
target_rank_treedef = treedefitem_query.first().treedef
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
- def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: Auditor, cache: Optional[Dict]=None, row_index: Optional[int] = None) -> Union["BoundTreeRecord", ParseFailures]:
+ def bind(
+ self,
+ collection,
+ row: Row,
+ uploadingAgentId: Optional[int],
+ auditor: Auditor,
+ cache: Optional[Dict] = None,
+ row_index: Optional[int] = None,
+ ) -> Union["BoundTreeRecord", ParseFailures]:
parsedFields: Dict[str, List[ParseResult]] = {}
parseFails: List[WorkBenchParseFailure] = []
@@ -257,6 +273,7 @@ def bind(self, collection, row: Row, uploadingAgentId: Optional[int], auditor: A
cache=cache,
)
+
class MustMatchTreeRecord(TreeRecord):
def apply_scoping(self, collection) -> "ScopedMustMatchTreeRecord":
s = super().apply_scoping(collection)
From 61034914593ab05a8dfe41a6314bd9ec51e7a18a Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 3 Sep 2024 18:14:28 -0500
Subject: [PATCH 45/78] remove unneeded conditional in handle_no_ranks_in_row
---
specifyweb/workbench/upload/treerecord.py | 25 +++++++++++------------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 869850cae2f..d159c5e2bb1 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -179,19 +179,18 @@ def find_non_null_ranks_in_row(row, rank_names):
def handle_no_ranks_in_row():
if self.treedef is not None:
return self, None
- else:
- first_rank = next(iter(self.ranks.keys()))
- target_rank_treedef_id = first_rank.treedef_id
- target_rank_treedef = tree_node_model.objects.filter(
- definition_id=target_rank_treedef_id, parent=None
- ).first()
- treedefitems = list(
- tree_rank_model.objects.filter(
- treedef_id=target_rank_treedef_id
- ).order_by("rankid")
- )
- root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
- return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
+ first_rank = next(iter(self.ranks.keys()))
+ target_rank_treedef_id = first_rank.treedef_id
+ target_rank_treedef = tree_node_model.objects.filter(
+ definition_id=target_rank_treedef_id, parent=None
+ ).first()
+ treedefitems = list(
+ tree_rank_model.objects.filter(
+ treedef_id=target_rank_treedef_id
+ ).order_by("rankid")
+ )
+ root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
def handle_multiple_ranks_in_row(ranks_in_row_not_null):
return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
From 508cbef3599908771996fb481563e908ce49cb5d Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 4 Sep 2024 01:41:51 -0500
Subject: [PATCH 46/78] new rank key request formatting
---
specifyweb/workbench/upload/treerecord.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index d159c5e2bb1..d488d709f2b 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -3,6 +3,7 @@
"""
import logging
+import re
from typing import List, Dict, Any, Tuple, NamedTuple, Optional, Union, Set
from django.db import transaction, IntegrityError
@@ -73,7 +74,12 @@ def _get_treedef_id(
return first_item.treedef_id
- treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+ if re.match(r'.*~>\d+$', rank_name):
+ rank_name, treedef_id = re.split(r'~>', rank_name)
+ treedef_id = int(treedef_id)
+ else:
+ treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+
return TreeRank(rank_name, treedef_id, tree_lower)
def check_rank(self) -> bool:
From 8942c8776f0013da77d327dfdebe34e88aea9ce0 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 4 Sep 2024 09:52:30 -0500
Subject: [PATCH 47/78] fix type error
---
specifyweb/workbench/upload/treerecord.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index d488d709f2b..dc36841daeb 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -75,8 +75,8 @@ def _get_treedef_id(
return first_item.treedef_id
if re.match(r'.*~>\d+$', rank_name):
- rank_name, treedef_id = re.split(r'~>', rank_name)
- treedef_id = int(treedef_id)
+ rank_name, treedef_id_str = re.split(r'~>', rank_name)
+ treedef_id = int(treedef_id_str)
else:
treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
From 79bbbccc08c64623a5f9b48fdf85671ddc782625 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Wed, 4 Sep 2024 12:55:54 -0400
Subject: [PATCH 48/78] Modify upload plan request to avoid duplicate rank keys
---
.../components/WbPlanView/mappingHelpers.ts | 22 +++++++++++++++++++
.../WbPlanView/uploadPlanBuilder.ts | 6 +++--
.../components/WbPlanView/uploadPlanParser.ts | 4 ++--
3 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
index dc65961182d..401a9623288 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
@@ -111,6 +111,28 @@ export const formatTreeDefinition = (definitionName: string): string =>
export const formatTreeRank = (rankName: string): string =>
`${schema.treeRankSymbol}${rankName}`;
+// Delimiter used for rank name keys i.e: ~>
+const RANK_KEY_DELIMITER = '~>';
+
+/**
+ * Returns a formatted tree rank name along with its tree id: (e.x $Kingdom => $Kingdom~>1)
+ * Used for generating unique key names when there are multiple trees with the same rank name.
+ * Opposite of getRankNameWithoutTreeId
+ * See: https://github.com/specify/specify7/pull/5091#issuecomment-2328037741
+ */
+export const formatTreeRankWithTreeId = (
+ rankName: string,
+ treeId: number
+): string => `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
+
+/**
+ * Returns the tree rank name after stripping its tree id: (e.x Kingdom~>1 => Kingdom)
+ * NOTE: Does not consider whether rankName is formatted with $ or not.
+ * Opposite of formatTreeRankWithTreeId
+ */
+export const getRankNameWithoutTreeId = (rankName: string): string =>
+ rankName.split(RANK_KEY_DELIMITER)[0];
+
// Match fields names like startDate_fullDate, but not _formatted
export const valueIsPartialField = (value: string): boolean =>
value.includes(schema.fieldPartSeparator) &&
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index b839b5719b1..493933674dd 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -5,7 +5,7 @@ import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
-import type { SplitMappingPath } from './mappingHelpers';
+import { formatTreeRankWithTreeId, SplitMappingPath } from './mappingHelpers';
import {
getNameFromTreeDefinitionName,
valueIsTreeDefinition,
@@ -71,7 +71,9 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
)?.definition.id;
return [
- rankName,
+ treeId === undefined
+ ? rankName
+ : formatTreeRankWithTreeId(rankName, treeId),
{
treeNodeCols: toTreeRecordRanks(rankMappedFields),
treeId,
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index b1378674013..e3e54e73127 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -6,7 +6,7 @@ import { softFail } from '../Errors/Crash';
import { getTreeDefinitions } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { MappingPath } from './Mapper';
-import type { SplitMappingPath } from './mappingHelpers';
+import { getRankNameWithoutTreeId, SplitMappingPath } from './mappingHelpers';
import { formatTreeDefinition } from './mappingHelpers';
import { formatToManyIndex, formatTreeRank } from './mappingHelpers';
@@ -82,7 +82,7 @@ const parseTree = (
getTreeDefinitions('Taxon', 'all').length > 1
? [resolveTreeId(rankData.treeId)]
: []),
- formatTreeRank(rankName),
+ formatTreeRank(getRankNameWithoutTreeId(rankName)),
]
)
);
From eb503a4a53bd8b7cb2c4c31dba0f96fa01cd2e2b Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Wed, 4 Sep 2024 15:21:42 -0400
Subject: [PATCH 49/78] fix upload plan test
---
.../js_src/lib/tests/fixtures/uploadplan.1.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
index a639bc1327d..26476e857f6 100644
--- a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
+++ b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
@@ -275,7 +275,7 @@
"taxon": {
"mustMatchTreeRecord": {
"ranks": {
- "Class": {
+ "Class~>1": {
"treeNodeCols": {
"name": {
"matchBehavior": "ignoreAlways",
@@ -286,32 +286,32 @@
},
"treeId": 1
},
- "Family": {
+ "Family~>1": {
"treeNodeCols": {
"name": "Family"
},
"treeId": 1
},
- "Genus": {
+ "Genus~>1": {
"treeNodeCols": {
"name": "Genus"
},
"treeId": 1
},
- "Subgenus": {
+ "Subgenus~>1": {
"treeNodeCols": {
"name": "Subgenus"
},
"treeId": 1
},
- "Species": {
+ "Species~>1": {
"treeNodeCols": {
"name": "Species",
"author": "Species Author"
},
"treeId": 1
},
- "Subspecies": {
+ "Subspecies~>1": {
"treeNodeCols": {
"name": "Subspecies",
"author": "Subspecies Author"
From 655a793e79081f4b6028ce54aa3a8e6eea4b941a Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 5 Sep 2024 11:56:16 -0500
Subject: [PATCH 50/78] reformulate rescope_tree_from_row
---
specifyweb/workbench/upload/treerecord.py | 107 +++++++++++++++++++++-
1 file changed, 104 insertions(+), 3 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index dc36841daeb..77c6cf9565e 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -2,6 +2,7 @@
For uploading tree records.
"""
+from collections import namedtuple
import logging
import re
from typing import List, Dict, Any, Tuple, NamedTuple, Optional, Union, Set
@@ -76,7 +77,7 @@ def _get_treedef_id(
if re.match(r'.*~>\d+$', rank_name):
rank_name, treedef_id_str = re.split(r'~>', rank_name)
- treedef_id = int(treedef_id_str)
+ treedef_id = _get_treedef_id(rank_name, tree, int(treedef_id_str), base_treedef_id)
else:
treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
@@ -100,6 +101,9 @@ def tree_rank_record(self) -> 'TreeRankRecord':
def get_treedefitem_model(tree: str):
return getattr(models, tree.lower().title() + 'treedefitem')
+def get_treedef_model(tree: str):
+ return getattr(models, tree.lower().title() + 'treedef')
+
class TreeRankRecord(NamedTuple):
rank_name: str
treedef_id: int
@@ -163,9 +167,16 @@ def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
return self._replace(disambiguation=disambiguation.disambiguate_tree()) if disambiguation is not None else self
def get_treedefs(self) -> Set:
- return set([self.treedef])
+ # return set([self.treedef]) # old way
- def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
+ tree_def_model = get_treedef_model(self.name)
+ treedefids = set([tree_rank_record.treedef_id for tree_rank_record in self.ranks.keys()])
+ # return set([tree_rank_record.treedef_id for tree_rank_record in self.ranks.keys()])
+ # return tree_rank_model.objects.filter(treedef_id__in=treedefids)
+ return set(tree_def_model.objects.filter(id__in=treedefids))
+
+ # Remove this old version of the funciton
+ def rescope_tree_from_row_old(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
def fetch_tree_node_model(name: str):
return getattr(models, name.lower().title())
@@ -204,12 +215,30 @@ def handle_multiple_ranks_in_row(ranks_in_row_not_null):
def handle_invalid_rank(target_rank_name):
return self, WorkBenchParseFailure('invalidRank', {'rank': target_rank_name}, target_rank_name)
+ # def handle_extended_rank_name(rank_col_name):
+ # extracted_rank_name = None
+ # if ' - ' in rank_col_name:
+ # extracted_rank_name, treedef_name = rank_col_name.split(' - ')
+ # elif '~>' in rank_col_name:
+ # extracted_rank_name, treedef_name = rank_col_name.split('~>')
+ # if extracted_rank_name in set([rank.rank_name for rank in self.ranks]):
+ # return extracted_rank_name
+ # return rank_col_name
+
+ # def modify_row_for_extended_rank_names(row):
+ # row_modified = {}
+ # for col_name in row.keys():
+ # col_name = handle_extended_rank_name(col_name)
+ # row_modified[col_name] = row[col_name]
+ # return row_modified
+
tree_rank_model = fetch_treedefitem_model(self.name)
tree_node_model = fetch_tree_node_model(self.name)
if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
return self, None
+ # mod_row = modify_row_for_extended_rank_names(row)
ranks = get_ranks_mapping(self.ranks)
rank_names = set(ranks.keys())
ranks_in_row_not_null = find_non_null_ranks_in_row(row, rank_names)
@@ -234,6 +263,78 @@ def handle_invalid_rank(target_rank_name):
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
+ RankColumn = namedtuple('RankColumn', ['treedef_id', 'treedefitem_name', 'tree_node_attribute', 'upload_value'])
+
+ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List["RankColumn"]:
+ # Get rank columns that are not null in the row
+ RankColumn = self.__class__.RankColumn
+ ranks_columns_in_row_not_null: List["RankColumn"] = []
+ for row_key, row_value in row.items():
+ if not row_value:
+ continue
+ for tree_rank_record, cols in self.ranks.items():
+ for col_name, col_opts in cols.items():
+ formatted_column = f'{col_opts.column} - {col_name}' if col_name != 'name' else col_opts.column
+ if formatted_column == row_key:
+ ranks_columns_in_row_not_null.append(
+ RankColumn(
+ tree_rank_record.treedef_id, # treedefid
+ tree_rank_record.rank_name, # treedefitem_name
+ col_name, # tree_node_attribute
+ row_value, # upload_value
+ )
+ )
+ break
+ return ranks_columns_in_row_not_null
+
+ def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List["RankColumn"]:
+ # Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
+ return list(
+ filter(
+ lambda rank_column: rank_column.treedef_id == target_rank_treedef_id,
+ ranks_columns_in_row_not_null,
+ )
+ )
+
+ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
+ # Starting out with a row with tree data.
+ # The row can have multiple tree ranks, and each rank can be associated with multiple columns, like name, authoer, etc.
+ # First need to match each rank column to a treedefitem.
+ # Second need to narrow down to the target rank based on which values in the row are not null.
+ # Lastly, need to return a new ScopedTreeRecord with the correct treedef, treedefitems, and root for this row.
+
+ tree_def_model = get_treedef_model(self.name)
+ tree_rank_model = get_treedefitem_model(self.name)
+ tree_node_model = getattr(models, self.name.lower().title())
+
+ if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
+ return self, None
+
+ # Get rank columns that are not null in the row
+ ranks_columns_in_row_not_null = self._get_not_null_ranks_columns_in_row(row)
+
+ # Determine the target treedef based on the columns that are not null
+ targeted_treedefids = set([rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null])
+ if targeted_treedefids is None or len(targeted_treedefids) == 0:
+ # return self, WorkBenchParseFailure('noRanksInRow', {}, None)
+ return self, None
+ elif len(targeted_treedefids) > 1:
+ # return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
+ logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
+
+ target_rank_treedef_id = targeted_treedefids.pop()
+ target_rank_treedef = tree_def_model.objects.get(id=target_rank_treedef_id)
+
+ # Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
+ # ranks_columns = self._filter_target_rank_columns(ranks_columns_in_row_not_null, target_rank_treedef_id)
+
+ # Based on the target treedef, get the treedefitems and root for the tree
+ treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
+ root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+
+ # Return a new ScopedTreeRecord with the correct treedefitem and treedef
+ return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
+
def bind(
self,
collection,
From 9babfb3148b07375f20719476bd61ec01167903d Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 13:55:18 -0400
Subject: [PATCH 51/78] Change tree headers when rank name is ambiguous
---
.../js_src/lib/components/WbPlanView/headerHelper.ts | 12 +++++++++++-
.../lib/components/WbPlanView/mappingPreview.ts | 7 ++++++-
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
index 97915632fd8..ab23c77f7a4 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
@@ -9,6 +9,7 @@ import type { RA } from '../../utils/types';
import { getUniqueName } from '../../utils/uniquifyName';
import type { Tables } from '../DataModel/types';
import type { MappingLine } from './Mapper';
+import { valueIsTreeRank } from './mappingHelpers';
import { generateMappingPathPreview } from './mappingPreview';
export function uniquifyHeaders(
@@ -34,13 +35,22 @@ export function renameNewlyCreatedHeaders(
headers: RA,
lines: RA
): RA {
+ const duplicateRanks = new Set(
+ lines
+ .flatMap(({ mappingPath }) => mappingPath)
+ .filter(
+ (rank, index, ranks) =>
+ valueIsTreeRank(rank) && ranks.indexOf(rank) !== index
+ )
+ );
+
const generatedHeaderPreviews = Object.fromEntries(
lines
.map((line, index) => ({ line, index }))
.filter(({ line }) => !headers.includes(line.headerName))
.map(({ line, index }) => [
index,
- generateMappingPathPreview(baseTableName, line.mappingPath),
+ generateMappingPathPreview(baseTableName, line.mappingPath, duplicateRanks),
])
);
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
index 3a27a0fade9..e4e6924327f 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
@@ -83,7 +83,8 @@ const mappingPathSubset = (
*/
export function generateMappingPathPreview(
baseTableName: keyof Tables,
- mappingPath: MappingPath
+ mappingPath: MappingPath,
+ duplicateRanks: Set
): string {
if (mappingPath.length === 0) return strictGetTable(baseTableName).label;
@@ -173,6 +174,10 @@ export function generateMappingPathPreview(
? [isAnyRank ? parentTableName : tableOrRankName]
: tableNameFormatted),
fieldNameFormatted,
+ ...(valueIsTreeRank(databaseTableOrRankName) &&
+ duplicateRanks.has(databaseTableOrRankName)
+ ? [parentTableName]
+ : []),
toManyIndexFormatted,
])
.filter(Boolean)
From ca338dcce2860284cef8a86e39171458e358dc02 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 13:59:41 -0400
Subject: [PATCH 52/78] fix typecheck
---
.../js_src/lib/components/WbPlanView/mappingPreview.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
index e4e6924327f..6d2405cbc94 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
@@ -84,7 +84,7 @@ const mappingPathSubset = (
export function generateMappingPathPreview(
baseTableName: keyof Tables,
mappingPath: MappingPath,
- duplicateRanks: Set
+ duplicateRanks?: Set
): string {
if (mappingPath.length === 0) return strictGetTable(baseTableName).label;
@@ -175,7 +175,7 @@ export function generateMappingPathPreview(
: tableNameFormatted),
fieldNameFormatted,
...(valueIsTreeRank(databaseTableOrRankName) &&
- duplicateRanks.has(databaseTableOrRankName)
+ duplicateRanks?.has(databaseTableOrRankName)
? [parentTableName]
: []),
toManyIndexFormatted,
From 39b45ce69e905754a9be7eba057c7fbb35c3d2d4 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 18:03:14 +0000
Subject: [PATCH 53/78] Lint code with ESLint and Prettier
Triggered by ca338dcce2860284cef8a86e39171458e358dc02 on branch refs/heads/issue-4980
---
.../frontend/js_src/lib/components/WbPlanView/mappingPreview.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
index 6d2405cbc94..083b38ddc8c 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
@@ -84,7 +84,7 @@ const mappingPathSubset = (
export function generateMappingPathPreview(
baseTableName: keyof Tables,
mappingPath: MappingPath,
- duplicateRanks?: Set
+ duplicateRanks?: ReadonlySet
): string {
if (mappingPath.length === 0) return strictGetTable(baseTableName).label;
From e215a77b47e1af743176ef666cb65e0ff4770a21 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 5 Sep 2024 13:55:35 -0500
Subject: [PATCH 54/78] fix RankColumn typecheck
---
specifyweb/workbench/upload/treerecord.py | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 77c6cf9565e..d320d9cee44 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -22,6 +22,11 @@
logger = logging.getLogger(__name__)
+class RankColumn(NamedTuple):
+ treedef_id: int
+ treedefitem_name: str
+ tree_node_attribute: str
+ upload_value: str
class TreeRank(NamedTuple):
rank_name: str
treedef_id: int
@@ -261,14 +266,11 @@ def handle_invalid_rank(target_rank_name):
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
target_rank_treedef = treedefitem_query.first().treedef
- return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
-
- RankColumn = namedtuple('RankColumn', ['treedef_id', 'treedefitem_name', 'tree_node_attribute', 'upload_value'])
+ return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
- def _get_not_null_ranks_columns_in_row(self, row: Row) -> List["RankColumn"]:
+ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[RankColumn]:
# Get rank columns that are not null in the row
- RankColumn = self.__class__.RankColumn
- ranks_columns_in_row_not_null: List["RankColumn"] = []
+ ranks_columns_in_row_not_null = []
for row_key, row_value in row.items():
if not row_value:
continue
@@ -287,7 +289,7 @@ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List["RankColumn"]:
break
return ranks_columns_in_row_not_null
- def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List["RankColumn"]:
+ def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[RankColumn]:
# Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
return list(
filter(
@@ -311,7 +313,7 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
return self, None
# Get rank columns that are not null in the row
- ranks_columns_in_row_not_null = self._get_not_null_ranks_columns_in_row(row)
+ ranks_columns_in_row_not_null: List[RankColumn] = self._get_not_null_ranks_columns_in_row(row)
# Determine the target treedef based on the columns that are not null
targeted_treedefids = set([rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null])
From 3595c2be9946fdf72663180a7762c658a8969aa0 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 5 Sep 2024 13:58:50 -0500
Subject: [PATCH 55/78] TreeRankCell
---
specifyweb/workbench/upload/treerecord.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index d320d9cee44..9604f0a0bc6 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -22,11 +22,12 @@
logger = logging.getLogger(__name__)
-class RankColumn(NamedTuple):
+class TreeRankCell(NamedTuple):
treedef_id: int
treedefitem_name: str
tree_node_attribute: str
upload_value: str
+
class TreeRank(NamedTuple):
rank_name: str
treedef_id: int
@@ -268,7 +269,7 @@ def handle_invalid_rank(target_rank_name):
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
- def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[RankColumn]:
+ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[TreeRankCell]:
# Get rank columns that are not null in the row
ranks_columns_in_row_not_null = []
for row_key, row_value in row.items():
@@ -279,7 +280,7 @@ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[RankColumn]:
formatted_column = f'{col_opts.column} - {col_name}' if col_name != 'name' else col_opts.column
if formatted_column == row_key:
ranks_columns_in_row_not_null.append(
- RankColumn(
+ TreeRankCell(
tree_rank_record.treedef_id, # treedefid
tree_rank_record.rank_name, # treedefitem_name
col_name, # tree_node_attribute
@@ -289,7 +290,7 @@ def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[RankColumn]:
break
return ranks_columns_in_row_not_null
- def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[RankColumn]:
+ def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[TreeRankCell]:
# Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
return list(
filter(
@@ -313,7 +314,7 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
return self, None
# Get rank columns that are not null in the row
- ranks_columns_in_row_not_null: List[RankColumn] = self._get_not_null_ranks_columns_in_row(row)
+ ranks_columns_in_row_not_null: List[TreeRankCell] = self._get_not_null_ranks_columns_in_row(row)
# Determine the target treedef based on the columns that are not null
targeted_treedefids = set([rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null])
From 270d06f26c909446f922bc6fb6bd47c25a3a9e9a Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 15:32:13 -0400
Subject: [PATCH 56/78] Fix mapping header being renamed in collections with
only 1 tree
---
.../components/WbPlanView/mappingPreview.ts | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
index 6d2405cbc94..2c659aaa3ac 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
@@ -22,6 +22,7 @@ import {
parsePartialField,
valueIsPartialField,
valueIsToManyIndex,
+ valueIsTreeDefinition,
valueIsTreeRank,
} from './mappingHelpers';
import { getMappingLineData } from './navigator';
@@ -121,8 +122,11 @@ export function generateMappingPathPreview(
: 1;
const toManyIndexFormatted = toManyIndexNumber > 1 ? toManyIndex : undefined;
- const [databaseFieldName, databaseTableOrRankName, databaseParentTableName] =
- mappingPathSubset([baseTableName, ...mappingPath]);
+ const [
+ databaseFieldName,
+ databaseTableOrRankName,
+ databaseParentTableOrTreeName,
+ ] = mappingPathSubset([baseTableName, ...mappingPath]);
// Attributes parts of filedLables to each variable or creates one if empty
const [
@@ -130,7 +134,7 @@ export function generateMappingPathPreview(
tableOrRankName = camelToHuman(
getNameFromTreeRankName(databaseTableOrRankName)
),
- parentTableName = camelToHuman(databaseParentTableName),
+ parentTableOrTreeName = camelToHuman(databaseParentTableOrTreeName),
] = mappingPathSubset(fieldLabels);
const isAnyRank = databaseTableOrRankName === formatTreeRank(anyTreeRank);
@@ -164,19 +168,20 @@ export function generateMappingPathPreview(
const tableNameFormatted =
tablesToHide.has(databaseTableOrRankName) &&
databaseFieldName !== formattedEntry
- ? [parentTableName || tableNameNonEmpty]
+ ? [parentTableOrTreeName || tableNameNonEmpty]
: genericTables.has(databaseTableOrRankName)
- ? [parentTableName, tableNameNonEmpty]
+ ? [parentTableOrTreeName, tableNameNonEmpty]
: [tableNameNonEmpty];
return filterArray([
...(valueIsTreeRank(databaseTableOrRankName)
- ? [isAnyRank ? parentTableName : tableOrRankName]
+ ? [isAnyRank ? parentTableOrTreeName : tableOrRankName]
: tableNameFormatted),
fieldNameFormatted,
...(valueIsTreeRank(databaseTableOrRankName) &&
+ valueIsTreeDefinition(databaseParentTableOrTreeName) &&
duplicateRanks?.has(databaseTableOrRankName)
- ? [parentTableName]
+ ? [parentTableOrTreeName]
: []),
toManyIndexFormatted,
])
From 833adb5ec3b53cf0b4e45a8727c286e38c229f19 Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 15:33:03 -0400
Subject: [PATCH 57/78] Fix upload plan when there are multiple ranks of the
same tree in the mapper
---
.../WbPlanView/uploadPlanBuilder.ts | 51 +++++++++++--------
1 file changed, 30 insertions(+), 21 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 493933674dd..5137ebb6b25 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -52,33 +52,42 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
return {
ranks: Object.fromEntries(
- indexMappings(lines).map(([fullName, rankMappedFields]) => {
- const rankName = valueIsTreeRank(fullName)
- ? getNameFromTreeRankName(fullName)
- : getNameFromTreeRankName(rankMappedFields[0].mappingPath[0]);
+ indexMappings(lines).flatMap(([fullName, rankMappedFields]) => {
+ // For collections with only 1 tree, rankMappedFields already has the correct format for generating an upload plan
+ // When there are multiple trees in the mapper, rankMappedFields is actually a mapping path of trees mapped to ranks
+ // which means we need to get the index mappings of it again to get the actual rankMappedFields
+ const rankMappings: RA]> =
+ valueIsTreeRank(fullName)
+ ? [[fullName, rankMappedFields]]
+ : indexMappings(rankMappedFields);
const treeName = valueIsTreeDefinition(fullName)
? getNameFromTreeDefinitionName(fullName)
: undefined;
- const treeId =
- treeName === undefined
- ? treeDefinitions.find(({ ranks }) =>
- ranks.find((r) => r.name === rankName)
- )?.definition.id
- : treeDefinitions.find(
- ({ definition }) => definition.name === treeName
- )?.definition.id;
+ return rankMappings.map(([fullName, mappedFields]) => {
+ const rankName = getNameFromTreeRankName(fullName);
- return [
- treeId === undefined
- ? rankName
- : formatTreeRankWithTreeId(rankName, treeId),
- {
- treeNodeCols: toTreeRecordRanks(rankMappedFields),
- treeId,
- },
- ];
+ // Resolve treeId using treeName or using rankName as fallback
+ const treeId =
+ treeName === undefined
+ ? treeDefinitions.find(({ ranks }) =>
+ ranks.find((r) => r.name === rankName)
+ )?.definition.id
+ : treeDefinitions.find(
+ ({ definition }) => definition.name === treeName
+ )?.definition.id;
+
+ return [
+ treeId === undefined
+ ? rankName
+ : formatTreeRankWithTreeId(rankName, treeId),
+ {
+ treeNodeCols: toTreeRecordRanks(mappedFields),
+ treeId,
+ },
+ ];
+ });
})
),
};
From bc3e9f6d392145248b6d1e52532b2c5b6024298c Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Thu, 5 Sep 2024 19:36:56 +0000
Subject: [PATCH 58/78] Lint code with ESLint and Prettier
Triggered by dd7e3f82080ae6e66080fb1b496c7da87dcda12c on branch refs/heads/issue-4980
---
.../lib/components/WbPlanView/uploadPlanBuilder.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 5137ebb6b25..0fbc2d45f3b 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -5,7 +5,8 @@ import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
-import { formatTreeRankWithTreeId, SplitMappingPath } from './mappingHelpers';
+import type { SplitMappingPath } from './mappingHelpers';
+import { formatTreeRankWithTreeId } from './mappingHelpers';
import {
getNameFromTreeDefinitionName,
valueIsTreeDefinition,
@@ -53,9 +54,11 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
return {
ranks: Object.fromEntries(
indexMappings(lines).flatMap(([fullName, rankMappedFields]) => {
- // For collections with only 1 tree, rankMappedFields already has the correct format for generating an upload plan
- // When there are multiple trees in the mapper, rankMappedFields is actually a mapping path of trees mapped to ranks
- // which means we need to get the index mappings of it again to get the actual rankMappedFields
+ /*
+ * For collections with only 1 tree, rankMappedFields already has the correct format for generating an upload plan
+ * When there are multiple trees in the mapper, rankMappedFields is actually a mapping path of trees mapped to ranks
+ * which means we need to get the index mappings of it again to get the actual rankMappedFields
+ */
const rankMappings: RA]> =
valueIsTreeRank(fullName)
? [[fullName, rankMappedFields]]
From 16671baab00df3426d40ec5bad29613eb5339a35 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 5 Sep 2024 16:05:12 -0500
Subject: [PATCH 59/78] update treerecord _to_match function and remove
rescope_tree_from_row_old
---
specifyweb/workbench/upload/treerecord.py | 116 ++++------------------
1 file changed, 20 insertions(+), 96 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 9604f0a0bc6..1fcf3b390eb 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -174,100 +174,9 @@ def disambiguate(self, disambiguation: DA) -> "ScopedTreeRecord":
def get_treedefs(self) -> Set:
# return set([self.treedef]) # old way
-
tree_def_model = get_treedef_model(self.name)
treedefids = set([tree_rank_record.treedef_id for tree_rank_record in self.ranks.keys()])
- # return set([tree_rank_record.treedef_id for tree_rank_record in self.ranks.keys()])
- # return tree_rank_model.objects.filter(treedef_id__in=treedefids)
return set(tree_def_model.objects.filter(id__in=treedefids))
-
- # Remove this old version of the funciton
- def rescope_tree_from_row_old(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
- def fetch_tree_node_model(name: str):
- return getattr(models, name.lower().title())
-
- def fetch_treedefitem_model(name: str):
- return get_treedefitem_model(name)
-
- def get_ranks_mapping(ranks):
- return {rank.rank_name: rank.treedef_id for rank in ranks.keys()}
-
- def find_non_null_ranks_in_row(row, rank_names):
- return {
- col
- for col, value in row.items()
- if value is not None and value != "" and col in rank_names
- }
-
- def handle_no_ranks_in_row():
- if self.treedef is not None:
- return self, None
- first_rank = next(iter(self.ranks.keys()))
- target_rank_treedef_id = first_rank.treedef_id
- target_rank_treedef = tree_node_model.objects.filter(
- definition_id=target_rank_treedef_id, parent=None
- ).first()
- treedefitems = list(
- tree_rank_model.objects.filter(
- treedef_id=target_rank_treedef_id
- ).order_by("rankid")
- )
- root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
- return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
-
- def handle_multiple_ranks_in_row(ranks_in_row_not_null):
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
-
- def handle_invalid_rank(target_rank_name):
- return self, WorkBenchParseFailure('invalidRank', {'rank': target_rank_name}, target_rank_name)
-
- # def handle_extended_rank_name(rank_col_name):
- # extracted_rank_name = None
- # if ' - ' in rank_col_name:
- # extracted_rank_name, treedef_name = rank_col_name.split(' - ')
- # elif '~>' in rank_col_name:
- # extracted_rank_name, treedef_name = rank_col_name.split('~>')
- # if extracted_rank_name in set([rank.rank_name for rank in self.ranks]):
- # return extracted_rank_name
- # return rank_col_name
-
- # def modify_row_for_extended_rank_names(row):
- # row_modified = {}
- # for col_name in row.keys():
- # col_name = handle_extended_rank_name(col_name)
- # row_modified[col_name] = row[col_name]
- # return row_modified
-
- tree_rank_model = fetch_treedefitem_model(self.name)
- tree_node_model = fetch_tree_node_model(self.name)
-
- if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
- return self, None
-
- # mod_row = modify_row_for_extended_rank_names(row)
- ranks = get_ranks_mapping(self.ranks)
- rank_names = set(ranks.keys())
- ranks_in_row_not_null = find_non_null_ranks_in_row(row, rank_names)
-
- if len(ranks_in_row_not_null) > 1:
- return handle_multiple_ranks_in_row(ranks_in_row_not_null)
- if not ranks_in_row_not_null:
- return handle_no_ranks_in_row()
-
- target_rank_name = ranks_in_row_not_null.pop()
- target_rank_treedef_id = ranks[target_rank_name]
- treedefitem_query = tree_rank_model.objects.filter(
- name=target_rank_name, treedef_id=target_rank_treedef_id
- ).order_by("rankid")
-
- if not treedefitem_query.exists() or treedefitem_query.count() != 1:
- return handle_invalid_rank(target_rank_name)
-
- treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
- root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
- target_rank_treedef = treedefitem_query.first().treedef
-
- return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[TreeRankCell]:
# Get rank columns that are not null in the row
@@ -347,7 +256,7 @@ def bind(
cache: Optional[Dict] = None,
row_index: Optional[int] = None,
) -> Union["BoundTreeRecord", ParseFailures]:
- parsedFields: Dict[str, List[ParseResult]] = {}
+ parsedFields: Dict[TreeRankRecord, List[ParseResult]] = {}
parseFails: List[WorkBenchParseFailure] = []
rescoped_tree_record, parse_fail = self.rescope_tree_from_row(row)
@@ -357,7 +266,7 @@ def bind(
for tree_rank_record, cols in self.ranks.items():
nameColumn = cols['name']
presults, pfails = parse_many(collection, self.name, cols, row)
- parsedFields[tree_rank_record.rank_name] = presults
+ parsedFields[tree_rank_record] = presults
parseFails += pfails
filters = {k: v for result in presults for k, v in result.filter_on.items()}
if filters.get('name', None) is None:
@@ -409,7 +318,7 @@ class BoundTreeRecord(NamedTuple):
treedef: Any
treedefitems: List
root: Optional[Any]
- parsedFields: Dict[str, List[ParseResult]]
+ parsedFields: Dict[TreeRankRecord, List[ParseResult]]
uploadingAgentId: Optional[int]
auditor: Auditor
cache: Optional[Dict]
@@ -452,10 +361,25 @@ def _handle_row(self, must_match: bool) -> UploadResult:
return UploadResult(match_result, {}, {})
def _to_match(self) -> List[TreeDefItemWithParseResults]:
+ def has_non_null_values(parse_results):
+ return any(
+ value is not None
+ for result in parse_results
+ for value in result.filter_on.values()
+ )
+
+ def get_parse_results(tdi):
+ tree_rank_record = TreeRankRecord(tdi.name, tdi.treedef_id)
+ return self.parsedFields.get(tree_rank_record, [])
+
+ def is_valid_tdi(tdi):
+ tree_rank_names = {trr.rank_name for trr in self.parsedFields.keys()}
+ return tdi.name in tree_rank_names and has_non_null_values(get_parse_results(tdi))
+
return [
- TreeDefItemWithParseResults(tdi, self.parsedFields[tdi.name])
+ TreeDefItemWithParseResults(tdi, get_parse_results(tdi))
for tdi in self.treedefitems
- if tdi.name in self.parsedFields and any(v is not None for r in self.parsedFields[tdi.name] for v in r.filter_on.values())
+ if is_valid_tdi(tdi)
]
def _match(self, tdiwprs: List[TreeDefItemWithParseResults]) -> Tuple[List[TreeDefItemWithParseResults], MatchResult]:
From 1a368d94d54ffff0ef3c60c7182572712b996639 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Thu, 5 Sep 2024 16:14:39 -0500
Subject: [PATCH 60/78] remove fullname hack
---
specifyweb/workbench/upload/treerecord.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 1fcf3b390eb..bcc6d3e92e8 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -563,8 +563,6 @@ def _upload(self, to_upload: List[TreeDefItemWithParseResults], matched: Union[M
def _do_insert(self, model, **kwargs):
obj = model(**kwargs)
- if hasattr(obj, 'fullname') and obj.fullname is None:
- obj.fullname = obj.name
obj.save(skip_tree_extras=True)
return obj
From c84860fa656dcc71e66a395673f80d349965df0d Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Fri, 6 Sep 2024 10:02:23 -0400
Subject: [PATCH 61/78] Use tree name in header always - note: will not be
added to dbs with only 1 tree
---
.../js_src/lib/components/WbPlanView/headerHelper.ts | 12 +-----------
.../lib/components/WbPlanView/mappingPreview.ts | 6 ++----
2 files changed, 3 insertions(+), 15 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
index ab23c77f7a4..97915632fd8 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/headerHelper.ts
@@ -9,7 +9,6 @@ import type { RA } from '../../utils/types';
import { getUniqueName } from '../../utils/uniquifyName';
import type { Tables } from '../DataModel/types';
import type { MappingLine } from './Mapper';
-import { valueIsTreeRank } from './mappingHelpers';
import { generateMappingPathPreview } from './mappingPreview';
export function uniquifyHeaders(
@@ -35,22 +34,13 @@ export function renameNewlyCreatedHeaders(
headers: RA,
lines: RA
): RA {
- const duplicateRanks = new Set(
- lines
- .flatMap(({ mappingPath }) => mappingPath)
- .filter(
- (rank, index, ranks) =>
- valueIsTreeRank(rank) && ranks.indexOf(rank) !== index
- )
- );
-
const generatedHeaderPreviews = Object.fromEntries(
lines
.map((line, index) => ({ line, index }))
.filter(({ line }) => !headers.includes(line.headerName))
.map(({ line, index }) => [
index,
- generateMappingPathPreview(baseTableName, line.mappingPath, duplicateRanks),
+ generateMappingPathPreview(baseTableName, line.mappingPath),
])
);
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
index 0c4e05a4d34..ad03b945177 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts
@@ -84,8 +84,7 @@ const mappingPathSubset = (
*/
export function generateMappingPathPreview(
baseTableName: keyof Tables,
- mappingPath: MappingPath,
- duplicateRanks?: ReadonlySet
+ mappingPath: MappingPath
): string {
if (mappingPath.length === 0) return strictGetTable(baseTableName).label;
@@ -179,8 +178,7 @@ export function generateMappingPathPreview(
: tableNameFormatted),
fieldNameFormatted,
...(valueIsTreeRank(databaseTableOrRankName) &&
- valueIsTreeDefinition(databaseParentTableOrTreeName) &&
- duplicateRanks?.has(databaseTableOrRankName)
+ valueIsTreeDefinition(databaseParentTableOrTreeName)
? [parentTableOrTreeName]
: []),
toManyIndexFormatted,
From 67f874ecb4daddca2147d216e7c0d2fdd5eb1d17 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 6 Sep 2024 09:22:24 -0500
Subject: [PATCH 62/78] add back WorkBenchParseFailure for multipleRanksInRow
and noRoot
---
specifyweb/workbench/upload/treerecord.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index bcc6d3e92e8..521ee62f71b 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -231,8 +231,8 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
# return self, WorkBenchParseFailure('noRanksInRow', {}, None)
return self, None
elif len(targeted_treedefids) > 1:
- # return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_in_row_not_null)[0])
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0])
target_rank_treedef_id = targeted_treedefids.pop()
target_rank_treedef = tree_def_model.objects.get(id=target_rank_treedef_id)
@@ -243,6 +243,9 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
# Based on the target treedef, get the treedefitems and root for the tree
treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ if root is None:
+ logger.warning(f"No root found for treedef {target_rank_treedef_id}")
+ return self, WorkBenchParseFailure('noRoot', {}, None)
# Return a new ScopedTreeRecord with the correct treedefitem and treedef
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
From 3b7557837ea3b87dfd94a6db30d08ebf2a4f22a6 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 6 Sep 2024 12:13:20 -0500
Subject: [PATCH 63/78] functional refactoring
---
.../workbench/upload/tests/example_plan.py | 1 -
specifyweb/workbench/upload/treerecord.py | 297 +++++++++++++-----
.../workbench/upload/upload_plan_schema.py | 47 ++-
3 files changed, 251 insertions(+), 94 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index 9d2d99433af..7aabb873bcb 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -2,7 +2,6 @@
from ..tomany import ToManyRecord
from ..treerecord import TreeRecord
from ..upload_plan_schema import parse_column_options
-# from specifyweb.specify import models as spmodels
json = dict(
baseTableName = 'Collectionobject',
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 521ee62f71b..da71a54b9f2 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -41,23 +41,56 @@ def create(
base_treedef_id: Optional[int] = None,
) -> 'TreeRank':
tree_model = get_treedefitem_model(tree)
- tree_lower = tree.lower()
def _build_filter_kwargs(rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
- filter_kwargs = {'name': rank_name}
- if treedef_id is not None:
- filter_kwargs['treedef_id'] = treedef_id # type: ignore
+ """
+ Build filter keyword arguments for querying treedef items.
+ """
+
+ # Create base keyword arguments for filtering
+ def create_base_kwargs(rank_name: str) -> Dict[str, Any]:
+ return {'name': rank_name}
+
+ # Add treedef ID to keyword arguments if present
+ def add_treedef_id_if_present(kwargs: Dict[str, Any], treedef_id: Optional[int]) -> Dict[str, Any]:
+ if treedef_id is not None:
+ kwargs['treedef_id'] = treedef_id
+ return kwargs
+
+ filter_kwargs = create_base_kwargs(rank_name)
+ filter_kwargs = add_treedef_id_if_present(filter_kwargs, treedef_id)
return filter_kwargs
def _filter_by_base_treedef_id(treedefitems, rank_name: str, base_treedef_id: Optional[int]):
- if base_treedef_id is None:
- raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
- treedefitems = treedefitems.filter(treedef_id=base_treedef_id)
- if not treedefitems.exists():
- raise ValueError(f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
- if treedefitems.count() > 1:
- raise ValueError(f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}")
- return treedefitems
+ """
+ Filter treedefitems by base_treedef_id and ensure only one item is found.
+ """
+
+ # Check if base treedef ID is present
+ def check_base_treedef_id(base_treedef_id):
+ if base_treedef_id is None:
+ raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
+
+ # Filter treedefitems by base treedef ID
+ def filter_items_by_treedef_id(treedefitems, base_treedef_id):
+ return treedefitems.filter(treedef_id=base_treedef_id)
+
+ # Validate filtered items
+ def validate_filtered_items(treedefitems):
+ if not treedefitems.exists():
+ raise ValueError(
+ f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}"
+ )
+ if treedefitems.count() > 1:
+ raise ValueError(
+ f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}"
+ )
+
+ # Check base treedef ID and filter treedefitems, then validate
+ check_base_treedef_id(base_treedef_id)
+ filtered_items = filter_items_by_treedef_id(treedefitems, base_treedef_id)
+ validate_filtered_items(filtered_items)
+ return filtered_items
def _get_treedef_id(
rank_name: str,
@@ -65,40 +98,81 @@ def _get_treedef_id(
treedef_id: Optional[int],
base_treedef_id: Optional[int],
) -> int:
- filter_kwargs = _build_filter_kwargs(rank_name, treedef_id)
- treedefitems = tree_model.objects.filter(**filter_kwargs)
+ """
+ Get the treedef ID for the given rank name and tree.
+ """
+ # Fetch treedefitems based on filter keyword arguments
+ def fetch_treedefitems(filter_kwargs):
+ return tree_model.objects.filter(**filter_kwargs)
+
+ # Get the first item from the queryset
+ def get_first_item(treedefitems):
+ first_item = treedefitems.first()
+ if first_item is None:
+ raise ValueError(f"No treedefitems found for rank {rank_name}")
+ return first_item
+
+ # Handle cases where multiple items are found
+ def handle_multiple_items(treedefitems):
+ if treedefitems.count() > 1:
+ return _filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+ return treedefitems
+
+ # Build filter keyword arguments and fetch treedefitems
+ filter_kwargs = _build_filter_kwargs(rank_name, treedef_id)
+ treedefitems = fetch_treedefitems(filter_kwargs)
+
+ # Handle cases where no items are found
if not treedefitems.exists() and treedef_id is not None:
filter_kwargs = _build_filter_kwargs(rank_name)
- treedefitems = tree_model.objects.filter(**filter_kwargs)
-
- if treedefitems.count() > 1:
- treedefitems = _filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+ treedefitems = fetch_treedefitems(filter_kwargs)
- first_item = treedefitems.first()
- if first_item is None:
- raise ValueError(f"No treedefitems found for rank {rank_name}")
+ treedefitems = handle_multiple_items(treedefitems)
+ first_item = get_first_item(treedefitems)
return first_item.treedef_id
- if re.match(r'.*~>\d+$', rank_name):
- rank_name, treedef_id_str = re.split(r'~>', rank_name)
- treedef_id = _get_treedef_id(rank_name, tree, int(treedef_id_str), base_treedef_id)
- else:
- treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
-
- return TreeRank(rank_name, treedef_id, tree_lower)
+ # Get the treedef_id using the helper function
+ final_treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+
+ # Return an instance of TreeRank
+ return TreeRank(rank_name=rank_name, treedef_id=final_treedef_id, tree=tree)
def check_rank(self) -> bool:
- tree_model = get_treedefitem_model(self.tree)
- rank = tree_model.objects.filter(name=self.rank_name, treedef_id=self.treedef_id)
- return rank.exists() and rank.count() == 1
+ """
+ Check if the rank exists and is unique for the given tree and treedef ID.
+ """
+
+ # Get the tree model
+ def get_tree_model(tree: str):
+ return get_treedefitem_model(tree)
+
+ # Filter the rank by name
+ def filter_rank(tree_model, rank_name: str, treedef_id: int):
+ return tree_model.objects.filter(name=rank_name, treedef_id=treedef_id)
+
+ # Check if the rank exists and is unique
+ def rank_exists_and_unique(rank):
+ return rank.exists() and rank.count() == 1
+
+ tree_model = get_tree_model(self.tree)
+ rank = filter_rank(tree_model, self.rank_name, self.treedef_id)
+ return rank_exists_and_unique(rank)
def validate_rank(self) -> None:
+ """
+ Validate the rank for the given tree and treedef ID.
+ """
+
if not self.check_rank():
raise ValueError(f"Invalid rank {self.rank_name} for treedef {self.treedef_id}")
def tree_rank_record(self) -> 'TreeRankRecord':
+ """
+ Create a TreeRankRecord instance.
+ """
+
assert self.rank_name is not None, "Rank name is required"
assert self.treedef_id is not None, "Treedef ID is required"
assert self.tree is not None and self.tree.lower() in SPECIFY_TREES, "Tree is required"
@@ -114,18 +188,22 @@ class TreeRankRecord(NamedTuple):
rank_name: str
treedef_id: int
+ # Create a TreeRankRecord instance
def to_json(self) -> Dict:
return {
"rank": self.rank_name,
"treedefId": self.treedef_id,
}
+ # Get the key for the TreeRankRecord instance
def to_key(self) -> Tuple[str, int]:
return (self.rank_name, self.treedef_id)
+ # Check if the rank exists and is unique for the given tree and treedef ID
def check_rank(self, tree: str) -> bool:
return TreeRank.create(self.rank_name, tree, self.treedef_id).check_rank()
+ # Validate the rank for the given tree and treedef ID
def validate_rank(self, tree) -> None:
TreeRank.create(self.rank_name, tree, self.treedef_id).validate_rank()
@@ -179,25 +257,47 @@ def get_treedefs(self) -> Set:
return set(tree_def_model.objects.filter(id__in=treedefids))
def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[TreeRankCell]:
- # Get rank columns that are not null in the row
- ranks_columns_in_row_not_null = []
- for row_key, row_value in row.items():
- if not row_value:
- continue
- for tree_rank_record, cols in self.ranks.items():
- for col_name, col_opts in cols.items():
- formatted_column = f'{col_opts.column} - {col_name}' if col_name != 'name' else col_opts.column
- if formatted_column == row_key:
- ranks_columns_in_row_not_null.append(
- TreeRankCell(
- tree_rank_record.treedef_id, # treedefid
- tree_rank_record.rank_name, # treedefitem_name
- col_name, # tree_node_attribute
- row_value, # upload_value
- )
- )
- break
- return ranks_columns_in_row_not_null
+ """
+ Get rank columns that are not null in the row.
+ """
+
+ # Check if the value is not null
+ def is_not_null(value: Any) -> bool:
+ return bool(value)
+
+ # Format the column name
+ def format_column(col_name: str, col_opts: Any) -> str:
+ return f'{col_opts.column} - {col_name}' if col_name != 'name' else col_opts.column
+
+ # Match the column
+ def match_column(row_key: str, formatted_column: str) -> bool:
+ return formatted_column == row_key
+
+ # Create a TreeRankCell instance
+ def create_tree_rank_cell(tree_rank_record: Any, col_name: str, row_value: Any) -> TreeRankCell:
+ return TreeRankCell(
+ tree_rank_record.treedef_id, # treedefid
+ tree_rank_record.rank_name, # treedefitem_name
+ col_name, # tree_node_attribute
+ row_value, # upload_value
+ )
+
+ # Process the row item
+ def process_row_item(row_key: str, row_value: Any) -> List[TreeRankCell]:
+ if not is_not_null(row_value):
+ return []
+ return [
+ create_tree_rank_cell(tree_rank_record, col_name, row_value)
+ for tree_rank_record, cols in self.ranks.items()
+ for col_name, col_opts in cols.items()
+ if match_column(row_key, format_column(col_name, col_opts))
+ ]
+
+ return [
+ cell
+ for row_key, row_value in row.items()
+ for cell in process_row_item(row_key, row_value)
+ ]
def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[TreeRankCell]:
# Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
@@ -209,45 +309,81 @@ def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank
)
def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
- # Starting out with a row with tree data.
- # The row can have multiple tree ranks, and each rank can be associated with multiple columns, like name, authoer, etc.
- # First need to match each rank column to a treedefitem.
- # Second need to narrow down to the target rank based on which values in the row are not null.
- # Lastly, need to return a new ScopedTreeRecord with the correct treedef, treedefitems, and root for this row.
-
- tree_def_model = get_treedef_model(self.name)
- tree_rank_model = get_treedefitem_model(self.name)
- tree_node_model = getattr(models, self.name.lower().title())
+ """Rescope tree from row data."""
- if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
- return self, None
+ # Get models based on the name
+ def get_models(name: str):
+ tree_def_model = get_treedef_model(name)
+ tree_rank_model = get_treedefitem_model(name)
+ tree_node_model = getattr(models, name.lower().title())
+ return tree_def_model, tree_rank_model, tree_node_model
# Get rank columns that are not null in the row
- ranks_columns_in_row_not_null: List[TreeRankCell] = self._get_not_null_ranks_columns_in_row(row)
+ def get_not_null_ranks_columns(row: Row) -> List[TreeRankCell]:
+ return self._get_not_null_ranks_columns_in_row(row)
# Determine the target treedef based on the columns that are not null
- targeted_treedefids = set([rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null])
- if targeted_treedefids is None or len(targeted_treedefids) == 0:
- # return self, WorkBenchParseFailure('noRanksInRow', {}, None)
+ def get_targeted_treedefids(ranks_columns: List[TreeRankCell]) -> Set[int]:
+ return set(rank_column.treedef_id for rank_column in ranks_columns)
+
+ # Handle cases where there are multiple or no treedefs
+ def handle_multiple_or_no_treedefs(targeted_treedefids: Set[int], ranks_columns: List[TreeRankCell]):
+ if not targeted_treedefids:
+ return self, None
+ elif len(targeted_treedefids) > 1:
+ logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, ranks_columns[0])
+ return None
+
+ # Retrieve the target rank treedef
+ def get_target_rank_treedef(tree_def_model, target_rank_treedef_id: int):
+ return tree_def_model.objects.get(id=target_rank_treedef_id)
+
+ # Retrieve the treedef items and root for the tree
+ def get_treedefitems_and_root(tree_rank_model, tree_node_model, target_rank_treedef_id: int):
+ # Fetch treedef items
+ def fetch_treedefitems():
+ return list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
+
+ # Fetch root node
+ def fetch_root():
+ return tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+
+ # Check if root is None and log warning
+ def check_root(root):
+ if root is None:
+ logger.warning(f"No root found for treedef {target_rank_treedef_id}")
+ return None, WorkBenchParseFailure('noRoot', {}, None)
+ return root
+
+ treedefitems = fetch_treedefitems()
+ root = fetch_root()
+ root_checked = check_root(root)
+
+ if root_checked is not root:
+ return root_checked
+
+ return treedefitems, root
+
+ tree_def_model, tree_rank_model, tree_node_model = get_models(self.name)
+
+ if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
return self, None
- elif len(targeted_treedefids) > 1:
- logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0])
-
- target_rank_treedef_id = targeted_treedefids.pop()
- target_rank_treedef = tree_def_model.objects.get(id=target_rank_treedef_id)
- # Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
- # ranks_columns = self._filter_target_rank_columns(ranks_columns_in_row_not_null, target_rank_treedef_id)
+ ranks_columns_in_row_not_null = get_not_null_ranks_columns(row)
+ targeted_treedefids = get_targeted_treedefids(ranks_columns_in_row_not_null)
- # Based on the target treedef, get the treedefitems and root for the tree
- treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
- root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
+ result = handle_multiple_or_no_treedefs(targeted_treedefids, ranks_columns_in_row_not_null)
+ if result:
+ return result
+
+ target_rank_treedef_id = targeted_treedefids.pop()
+ target_rank_treedef = get_target_rank_treedef(tree_def_model, target_rank_treedef_id)
+
+ treedefitems, root = get_treedefitems_and_root(tree_rank_model, tree_node_model, target_rank_treedef_id)
if root is None:
- logger.warning(f"No root found for treedef {target_rank_treedef_id}")
return self, WorkBenchParseFailure('noRoot', {}, None)
- # Return a new ScopedTreeRecord with the correct treedefitem and treedef
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
def bind(
@@ -364,6 +500,8 @@ def _handle_row(self, must_match: bool) -> UploadResult:
return UploadResult(match_result, {}, {})
def _to_match(self) -> List[TreeDefItemWithParseResults]:
+
+ # Check if the parse results have non-null values
def has_non_null_values(parse_results):
return any(
value is not None
@@ -371,14 +509,17 @@ def has_non_null_values(parse_results):
for value in result.filter_on.values()
)
+ # Get parse results for a TDI
def get_parse_results(tdi):
tree_rank_record = TreeRankRecord(tdi.name, tdi.treedef_id)
return self.parsedFields.get(tree_rank_record, [])
+ # Check if the TDI is valid
def is_valid_tdi(tdi):
tree_rank_names = {trr.rank_name for trr in self.parsedFields.keys()}
return tdi.name in tree_rank_names and has_non_null_values(get_parse_results(tdi))
+ # Create a TreeDefItemWithParseResults instance
return [
TreeDefItemWithParseResults(tdi, get_parse_results(tdi))
for tdi in self.treedefitems
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index a9034b1f57d..997d0855c2a 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -1,3 +1,4 @@
+from functools import reduce
from os import name
from typing import Dict, Any, Optional, Union, Tuple
import logging
@@ -309,6 +310,9 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
)
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
+ """Parse tree record from the given data"""
+
+ # Parse rank details
def parse_rank(name_or_cols):
if isinstance(name_or_cols, str):
return name_or_cols, {'name': parse_column_options(name_or_cols)}, None
@@ -317,21 +321,34 @@ def parse_rank(name_or_cols):
treedefid = name_or_cols.get('treeId')
return rank_name, tree_node_cols, treedefid
- ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]] = {}
- for rank, name_or_cols in to_parse['ranks'].items():
- rank_name, parsed_cols, treedefid = parse_rank(name_or_cols)
- tree_rank_record = TreeRank.create(rank, table.name, treedefid, base_treedefid).tree_rank_record()
- ranks[tree_rank_record] = parsed_cols
-
- # Update the schema with the treeId
- # if not isinstance(name_or_cols, str):
- # to_parse['ranks'][rank]['treeId'] = tree_rank_record.treedef_id
-
- if base_treedefid is None:
- base_treedefid = tree_rank_record.treedef_id
-
- for rank, cols in ranks.items():
- assert 'name' in cols, to_parse
+ # Create tree rank record
+ def create_tree_rank_record(rank, table_name, treedefid, base_treedefid):
+ return TreeRank.create(rank, table_name, treedefid, base_treedefid).tree_rank_record()
+
+ def parse_ranks(to_parse, table_name, base_treedefid):
+ def parse_single_rank(rank, name_or_cols, base_treedefid):
+ rank_name, parsed_cols, treedefid = parse_rank(name_or_cols)
+ tree_rank_record = create_tree_rank_record(rank, table_name, treedefid, base_treedefid)
+ return tree_rank_record, parsed_cols, tree_rank_record.treedef_id
+
+ def aggregate_ranks(acc, item):
+ rank, name_or_cols = item
+ tree_rank_record, parsed_cols, treedefid = parse_single_rank(rank, name_or_cols, acc[1])
+ acc[0][tree_rank_record] = parsed_cols
+ if acc[1] is None:
+ acc[1] = treedefid
+ return acc
+
+ ranks, base_treedefid = reduce(aggregate_ranks, to_parse['ranks'].items(), ({}, base_treedefid))
+ return ranks, base_treedefid
+
+ # Validate ranks by checking if 'name' is present in each rank
+ def validate_ranks(ranks, to_parse):
+ for rank, cols in ranks.items():
+ assert 'name' in cols, to_parse
+
+ ranks, base_treedefid = parse_ranks(to_parse, table.name, base_treedefid)
+ validate_ranks(ranks, to_parse)
return TreeRecord(
name=table.django_name,
From 2f1cfccd2b20f16d51545defb57cbb25002c2613 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 6 Sep 2024 13:33:19 -0500
Subject: [PATCH 64/78] remove unneeded exception
---
specifyweb/workbench/upload/tests/example_plan.py | 1 -
specifyweb/workbench/upload/treerecord.py | 3 ---
2 files changed, 4 deletions(-)
diff --git a/specifyweb/workbench/upload/tests/example_plan.py b/specifyweb/workbench/upload/tests/example_plan.py
index 9d2d99433af..7aabb873bcb 100644
--- a/specifyweb/workbench/upload/tests/example_plan.py
+++ b/specifyweb/workbench/upload/tests/example_plan.py
@@ -2,7 +2,6 @@
from ..tomany import ToManyRecord
from ..treerecord import TreeRecord
from ..upload_plan_schema import parse_column_options
-# from specifyweb.specify import models as spmodels
json = dict(
baseTableName = 'Collectionobject',
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 521ee62f71b..fddedb55fe7 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -243,9 +243,6 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
# Based on the target treedef, get the treedefitems and root for the tree
treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
- if root is None:
- logger.warning(f"No root found for treedef {target_rank_treedef_id}")
- return self, WorkBenchParseFailure('noRoot', {}, None)
# Return a new ScopedTreeRecord with the correct treedefitem and treedef
return self._replace(treedef=target_rank_treedef, treedefitems=treedefitems, root=root), None
From 0432501450375f4f8b5f9e24870b712c6080b00e Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 6 Sep 2024 16:58:32 -0500
Subject: [PATCH 65/78] fix typecheck in WorkBenchParseFailure call
---
specifyweb/workbench/upload/treerecord.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index fddedb55fe7..1c74724a6d7 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -232,7 +232,7 @@ def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional[
return self, None
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0])
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0].treedefitem_name)
target_rank_treedef_id = targeted_treedefids.pop()
target_rank_treedef = tree_def_model.objects.get(id=target_rank_treedef_id)
From 8a2ba88fd64ba6d510cc3d89441fe393f4f56496 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Fri, 6 Sep 2024 17:25:43 -0500
Subject: [PATCH 66/78] formatting
---
specifyweb/workbench/upload/treerecord.py | 39 +++++++++++++------
.../workbench/upload/upload_plan_schema.py | 4 +-
2 files changed, 28 insertions(+), 15 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index d1b8753df3c..fce2ca17465 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -40,9 +40,12 @@ def create(
treedef_id: Optional[int] = None,
base_treedef_id: Optional[int] = None,
) -> 'TreeRank':
+ """
+ Create a TreeRank instance with the given rank name, tree, and optional treedef IDs.
+ """
tree_model = get_treedefitem_model(tree)
- def _build_filter_kwargs(rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
+ def build_filter_kwargs(rank_name: str, treedef_id: Optional[int] = None) -> Dict[str, Any]:
"""
Build filter keyword arguments for querying treedef items.
"""
@@ -61,7 +64,7 @@ def add_treedef_id_if_present(kwargs: Dict[str, Any], treedef_id: Optional[int])
filter_kwargs = add_treedef_id_if_present(filter_kwargs, treedef_id)
return filter_kwargs
- def _filter_by_base_treedef_id(treedefitems, rank_name: str, base_treedef_id: Optional[int]):
+ def filter_by_base_treedef_id(treedefitems, rank_name: str, base_treedef_id: Optional[int]):
"""
Filter treedefitems by base_treedef_id and ensure only one item is found.
"""
@@ -92,7 +95,7 @@ def validate_filtered_items(treedefitems):
validate_filtered_items(filtered_items)
return filtered_items
- def _get_treedef_id(
+ def get_treedef_id(
rank_name: str,
tree: str,
treedef_id: Optional[int],
@@ -116,16 +119,16 @@ def get_first_item(treedefitems):
# Handle cases where multiple items are found
def handle_multiple_items(treedefitems):
if treedefitems.count() > 1:
- return _filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+ return filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
return treedefitems
# Build filter keyword arguments and fetch treedefitems
- filter_kwargs = _build_filter_kwargs(rank_name, treedef_id)
+ filter_kwargs = build_filter_kwargs(rank_name, treedef_id)
treedefitems = fetch_treedefitems(filter_kwargs)
# Handle cases where no items are found
if not treedefitems.exists() and treedef_id is not None:
- filter_kwargs = _build_filter_kwargs(rank_name)
+ filter_kwargs = build_filter_kwargs(rank_name)
treedefitems = fetch_treedefitems(filter_kwargs)
treedefitems = handle_multiple_items(treedefitems)
@@ -133,11 +136,20 @@ def handle_multiple_items(treedefitems):
return first_item.treedef_id
- # Get the treedef_id using the helper function
- final_treedef_id = _get_treedef_id(rank_name, tree, treedef_id, base_treedef_id)
+ def extract_treedef_id(rank_name: str) -> Tuple[Union[str, Any], Optional[int]]:
+ """
+ Extract treedef_id from rank_name if it exists in the format 'rank_name~>treedef_id'.
+ """
+ match = re.match(r'(.*)~>(\d+)$', rank_name)
+ if match:
+ rank_name, treedef_id_str = match.groups()
+ return rank_name, int(treedef_id_str)
+ return rank_name, None
+
+ rank_name, extracted_treedef_id = extract_treedef_id(rank_name)
+ target_treedef_id = get_treedef_id(rank_name, tree, extracted_treedef_id or treedef_id, base_treedef_id)
- # Return an instance of TreeRank
- return TreeRank(rank_name=rank_name, treedef_id=final_treedef_id, tree=tree)
+ return TreeRank(rank_name, target_treedef_id, tree.lower())
def check_rank(self) -> bool:
"""
@@ -300,7 +312,10 @@ def process_row_item(row_key: str, row_value: Any) -> List[TreeRankCell]:
]
def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[TreeRankCell]:
- # Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
+ """
+ Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
+ """
+
return list(
filter(
lambda rank_column: rank_column.treedef_id == target_rank_treedef_id,
@@ -332,7 +347,7 @@ def handle_multiple_or_no_treedefs(targeted_treedefids: Set[int], ranks_columns:
return self, None
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, ranks_columns[0])
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, ranks_columns[0].treedefitem_name)
return None
# Retrieve the target rank treedef
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 997d0855c2a..0b0f89e4e25 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -312,7 +312,6 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
"""Parse tree record from the given data"""
- # Parse rank details
def parse_rank(name_or_cols):
if isinstance(name_or_cols, str):
return name_or_cols, {'name': parse_column_options(name_or_cols)}, None
@@ -321,7 +320,6 @@ def parse_rank(name_or_cols):
treedefid = name_or_cols.get('treeId')
return rank_name, tree_node_cols, treedefid
- # Create tree rank record
def create_tree_rank_record(rank, table_name, treedefid, base_treedefid):
return TreeRank.create(rank, table_name, treedefid, base_treedefid).tree_rank_record()
@@ -339,7 +337,7 @@ def aggregate_ranks(acc, item):
acc[1] = treedefid
return acc
- ranks, base_treedefid = reduce(aggregate_ranks, to_parse['ranks'].items(), ({}, base_treedefid))
+ ranks, base_treedefid = reduce(aggregate_ranks, to_parse['ranks'].items(), [{}, base_treedefid])
return ranks, base_treedefid
# Validate ranks by checking if 'name' is present in each rank
From d2f7531521ba8633896349de3908063b0dd05f60 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Mon, 9 Sep 2024 12:04:35 -0500
Subject: [PATCH 67/78] edit _filter_target_rank_columns and extract_treedef_id
---
specifyweb/workbench/upload/treerecord.py | 24 +++++------------------
1 file changed, 5 insertions(+), 19 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 928644e155e..c72fba42e10 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -140,10 +140,11 @@ def extract_treedef_id(rank_name: str) -> Tuple[Union[str, Any], Optional[int]]:
"""
Extract treedef_id from rank_name if it exists in the format 'rank_name~>treedef_id'.
"""
- match = re.match(r'(.*)~>(\d+)$', rank_name)
- if match:
- rank_name, treedef_id_str = match.groups()
- return rank_name, int(treedef_id_str)
+ parts = rank_name.rsplit('~>', 1)
+ if len(parts) == 2 and parts[1].isdigit():
+ rank_name = parts[0]
+ tree_def_id = int(parts[1])
+ return rank_name, tree_def_id
return rank_name, None
rank_name, extracted_treedef_id = extract_treedef_id(rank_name)
@@ -311,18 +312,6 @@ def process_row_item(row_key: str, row_value: Any) -> List[TreeRankCell]:
for cell in process_row_item(row_key, row_value)
]
- def _filter_target_rank_columns(self, ranks_columns_in_row_not_null, target_rank_treedef_id) -> List[TreeRankCell]:
- """
- Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
- """
-
- return list(
- filter(
- lambda rank_column: rank_column.treedef_id == target_rank_treedef_id,
- ranks_columns_in_row_not_null,
- )
- )
-
def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
"""Rescope tree from row data."""
@@ -404,9 +393,6 @@ def check_root(root):
target_rank_treedef_id = targeted_treedefids.pop()
target_rank_treedef = get_target_rank_treedef(tree_def_model, target_rank_treedef_id)
- # Filter ranks_columns_in_row_not_null to only include columns that are part of the target treedef
- # ranks_columns = self._filter_target_rank_columns(ranks_columns_in_row_not_null, target_rank_treedef_id)
-
# Based on the target treedef, get the treedefitems and root for the tree
treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
From e38d54c22ef2f251858294803f60f99ea995bcd5 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Mon, 9 Sep 2024 16:59:46 -0500
Subject: [PATCH 68/78] remove base_treedef_id
---
specifyweb/workbench/upload/scoping.py | 4 +-
specifyweb/workbench/upload/treerecord.py | 31 +++++-----
.../workbench/upload/upload_plan_schema.py | 57 +++++++------------
3 files changed, 36 insertions(+), 56 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index 9e1f9338491..ab70b7b3c83 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -188,8 +188,6 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
treedef = None
if table.name == 'Taxon':
- if tr.base_treedef_id is not None:
- treedef = models.Taxontreedef.objects.filter(id=tr.base_treedef_id).first()
if treedef is None:
treedef = collection.discipline.taxontreedef
@@ -222,7 +220,7 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
scoped_ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]] = {}
for r, cols in tr.ranks.items():
if isinstance(r, str):
- r = TreeRank.create(r, table.name, treedef.id if treedef else None, tr.base_treedef_id).tree_rank_record()
+ r = TreeRank.create(r, table.name, treedef.id if treedef else None).tree_rank_record()
scoped_ranks[r] = {}
for f, colopts in cols.items():
scoped_ranks[r][f] = extend_columnoptions(colopts, collection, table.name, f)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index c72fba42e10..5595589a89c 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -38,7 +38,6 @@ def create(
rank_name: str,
tree: str,
treedef_id: Optional[int] = None,
- base_treedef_id: Optional[int] = None,
) -> 'TreeRank':
"""
Create a TreeRank instance with the given rank name, tree, and optional treedef IDs.
@@ -64,34 +63,34 @@ def add_treedef_id_if_present(kwargs: Dict[str, Any], treedef_id: Optional[int])
filter_kwargs = add_treedef_id_if_present(filter_kwargs, treedef_id)
return filter_kwargs
- def filter_by_base_treedef_id(treedefitems, rank_name: str, base_treedef_id: Optional[int]):
+ def filter_by_treedef_id(treedefitems, rank_name: str):
"""
- Filter treedefitems by base_treedef_id and ensure only one item is found.
+ Filter treedefitems by treedef_id and ensure only one item is found.
"""
- # Check if base treedef ID is present
- def check_base_treedef_id(base_treedef_id):
- if base_treedef_id is None:
+ # Check if treedef ID is present
+ def check_treedef_id(treedef_id):
+ if treedef_id is None:
raise ValueError(f"Multiple treedefitems found for rank {rank_name}")
- # Filter treedefitems by base treedef ID
- def filter_items_by_treedef_id(treedefitems, base_treedef_id):
- return treedefitems.filter(treedef_id=base_treedef_id)
+ # Filter treedefitems by treedef ID
+ def filter_items_by_treedef_id(treedefitems, treedef_id):
+ return treedefitems.filter(treedef_id=treedef_id)
# Validate filtered items
def validate_filtered_items(treedefitems):
if not treedefitems.exists():
raise ValueError(
- f"No treedefitems found for rank {rank_name} and base treedef {base_treedef_id}"
+ f"No treedefitems found for rank {rank_name} and treedef {treedef_id}"
)
if treedefitems.count() > 1:
raise ValueError(
- f"Multiple treedefitems found for rank {rank_name} and base treedef {base_treedef_id}"
+ f"Multiple treedefitems found for rank {rank_name} and treedef {treedef_id}"
)
# Check base treedef ID and filter treedefitems, then validate
- check_base_treedef_id(base_treedef_id)
- filtered_items = filter_items_by_treedef_id(treedefitems, base_treedef_id)
+ check_treedef_id(treedef_id)
+ filtered_items = filter_items_by_treedef_id(treedefitems, treedef_id)
validate_filtered_items(filtered_items)
return filtered_items
@@ -99,7 +98,6 @@ def get_treedef_id(
rank_name: str,
tree: str,
treedef_id: Optional[int],
- base_treedef_id: Optional[int],
) -> int:
"""
Get the treedef ID for the given rank name and tree.
@@ -119,7 +117,7 @@ def get_first_item(treedefitems):
# Handle cases where multiple items are found
def handle_multiple_items(treedefitems):
if treedefitems.count() > 1:
- return filter_by_base_treedef_id(treedefitems, rank_name, base_treedef_id)
+ return filter_by_treedef_id(treedefitems, rank_name, treedef_id)
return treedefitems
# Build filter keyword arguments and fetch treedefitems
@@ -148,7 +146,7 @@ def extract_treedef_id(rank_name: str) -> Tuple[Union[str, Any], Optional[int]]:
return rank_name, None
rank_name, extracted_treedef_id = extract_treedef_id(rank_name)
- target_treedef_id = get_treedef_id(rank_name, tree, extracted_treedef_id or treedef_id, base_treedef_id)
+ target_treedef_id = get_treedef_id(rank_name, tree, extracted_treedef_id or treedef_id)
return TreeRank(rank_name, target_treedef_id, tree.lower())
@@ -223,7 +221,6 @@ def validate_rank(self, tree) -> None:
class TreeRecord(NamedTuple):
name: str
ranks: Dict[Union[str, TreeRankRecord], Dict[str, ColumnOptions]]
- base_treedef_id: Optional[int] = None
def apply_scoping(self, collection) -> "ScopedTreeRecord":
from .scoping import apply_scoping_to_treerecord as apply_scoping
diff --git a/specifyweb/workbench/upload/upload_plan_schema.py b/specifyweb/workbench/upload/upload_plan_schema.py
index 0b0f89e4e25..e148307fa46 100644
--- a/specifyweb/workbench/upload/upload_plan_schema.py
+++ b/specifyweb/workbench/upload/upload_plan_schema.py
@@ -35,12 +35,6 @@
'additionalProperties': False
},
]
- },
- "treeId": {
- "oneOf": [
- { "type": "integer" },
- { "type": "null" }
- ]
}
},
'required': [ 'baseTableName', 'uploadable' ],
@@ -233,17 +227,14 @@
def parse_plan_with_basetable(collection, to_parse: Dict) -> Tuple[Table, Uploadable]:
base_table = datamodel.get_table_strict(to_parse['baseTableName'])
- extra = {}
- if 'treeId' in to_parse.keys():
- extra['treedefid'] = to_parse['treeId']
- return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'], extra)
+ return base_table, parse_uploadable(collection, base_table, to_parse['uploadable'])
def parse_plan(collection, to_parse: Dict) -> Uploadable:
return parse_plan_with_basetable(collection, to_parse)[1]
-def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> Uploadable:
+def parse_uploadable(collection, table: Table, to_parse: Dict) -> Uploadable:
if 'uploadTable' in to_parse:
- return parse_upload_table(collection, table, to_parse['uploadTable'], extra)
+ return parse_upload_table(collection, table, to_parse['uploadTable'])
if 'oneToOneTable' in to_parse:
return OneToOneTable(*parse_upload_table(collection, table, to_parse['oneToOneTable']))
@@ -255,14 +246,11 @@ def parse_uploadable(collection, table: Table, to_parse: Dict, extra: Dict = {})
return MustMatchTreeRecord(*parse_tree_record(collection, table, to_parse['mustMatchTreeRecord']))
if 'treeRecord' in to_parse:
- treedefid = None
- if 'treedefid' in extra.keys():
- treedefid = int(extra['treedefid'])
- return parse_tree_record(collection, table, to_parse['treeRecord'], treedefid)
+ return parse_tree_record(collection, table, to_parse['treeRecord'])
raise ValueError('unknown uploadable type')
-def parse_upload_table(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> UploadTable:
+def parse_upload_table(collection, table: Table, to_parse: Dict) -> UploadTable:
def rel_table(key: str) -> Table:
return datamodel.get_table_strict(table.get_relationship(key).relatedModelName)
@@ -304,12 +292,12 @@ def defer_scope_upload_table(default_collection, table: Table, to_parse: Dict, d
for key, to_one in to_parse['toOne'].items()
},
toMany={
- key: [parse_to_many_record(collection, rel_table(key), record, extra) for record in to_manys]
+ key: [parse_to_many_record(collection, rel_table(key), record) for record in to_manys]
for key, to_manys in to_parse['toMany'].items()
}
)
-def parse_tree_record(collection, table: Table, to_parse: Dict, base_treedefid: Optional[int] = None) -> TreeRecord:
+def parse_tree_record(collection, table: Table, to_parse: Dict) -> TreeRecord:
"""Parse tree record from the given data"""
def parse_rank(name_or_cols):
@@ -320,41 +308,38 @@ def parse_rank(name_or_cols):
treedefid = name_or_cols.get('treeId')
return rank_name, tree_node_cols, treedefid
- def create_tree_rank_record(rank, table_name, treedefid, base_treedefid):
- return TreeRank.create(rank, table_name, treedefid, base_treedefid).tree_rank_record()
+ def create_tree_rank_record(rank, table_name, treedefid):
+ return TreeRank.create(rank, table_name, treedefid).tree_rank_record()
- def parse_ranks(to_parse, table_name, base_treedefid):
- def parse_single_rank(rank, name_or_cols, base_treedefid):
+ def parse_ranks(to_parse, table_name):
+ def parse_single_rank(rank, name_or_cols):
rank_name, parsed_cols, treedefid = parse_rank(name_or_cols)
- tree_rank_record = create_tree_rank_record(rank, table_name, treedefid, base_treedefid)
- return tree_rank_record, parsed_cols, tree_rank_record.treedef_id
+ tree_rank_record = create_tree_rank_record(rank, table_name, treedefid)
+ return tree_rank_record, parsed_cols
def aggregate_ranks(acc, item):
rank, name_or_cols = item
- tree_rank_record, parsed_cols, treedefid = parse_single_rank(rank, name_or_cols, acc[1])
- acc[0][tree_rank_record] = parsed_cols
- if acc[1] is None:
- acc[1] = treedefid
+ tree_rank_record, parsed_cols = parse_single_rank(rank, name_or_cols)
+ acc[tree_rank_record] = parsed_cols
return acc
- ranks, base_treedefid = reduce(aggregate_ranks, to_parse['ranks'].items(), [{}, base_treedefid])
- return ranks, base_treedefid
+ ranks = reduce(aggregate_ranks, to_parse['ranks'].items(), {})
+ return ranks
# Validate ranks by checking if 'name' is present in each rank
def validate_ranks(ranks, to_parse):
for rank, cols in ranks.items():
assert 'name' in cols, to_parse
- ranks, base_treedefid = parse_ranks(to_parse, table.name, base_treedefid)
+ ranks = parse_ranks(to_parse, table.name)
validate_ranks(ranks, to_parse)
return TreeRecord(
name=table.django_name,
- ranks=ranks,
- base_treedef_id=base_treedefid
+ ranks=ranks
)
-def parse_to_many_record(collection, table: Table, to_parse: Dict, extra: Dict = {}) -> ToManyRecord:
+def parse_to_many_record(collection, table: Table, to_parse: Dict) -> ToManyRecord:
def rel_table(key: str) -> Table:
return datamodel.get_table_strict(table.get_relationship(key).relatedModelName)
@@ -364,7 +349,7 @@ def rel_table(key: str) -> Table:
wbcols={k: parse_column_options(v) for k,v in to_parse['wbcols'].items()},
static=to_parse['static'],
toOne={
- key: parse_uploadable(collection, rel_table(key), to_one, extra)
+ key: parse_uploadable(collection, rel_table(key), to_one)
for key, to_one in to_parse['toOne'].items()
},
)
From f3e052a66eb1a3d12928ebda4978b69e6d854208 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 10 Sep 2024 09:35:47 -0500
Subject: [PATCH 69/78] scoped_ranks rewrite
---
specifyweb/workbench/upload/scoping.py | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/specifyweb/workbench/upload/scoping.py b/specifyweb/workbench/upload/scoping.py
index ab70b7b3c83..c75d90d4b16 100644
--- a/specifyweb/workbench/upload/scoping.py
+++ b/specifyweb/workbench/upload/scoping.py
@@ -217,13 +217,19 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
root = list(getattr(models, table.name.capitalize()).objects.filter(definitionitem=treedefitems[0])[:1]) # assume there is only one
- scoped_ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]] = {}
- for r, cols in tr.ranks.items():
- if isinstance(r, str):
- r = TreeRank.create(r, table.name, treedef.id if treedef else None).tree_rank_record()
- scoped_ranks[r] = {}
- for f, colopts in cols.items():
- scoped_ranks[r][f] = extend_columnoptions(colopts, collection, table.name, f)
+ scoped_ranks: Dict[TreeRankRecord, Dict[str, ExtendedColumnOptions]] = {
+ (
+ TreeRank.create(
+ r, table.name, treedef.id if treedef else None
+ ).tree_rank_record()
+ if isinstance(r, str)
+ else r
+ ): {
+ f: extend_columnoptions(colopts, collection, table.name, f)
+ for f, colopts in cols.items()
+ }
+ for r, cols in tr.ranks.items()
+ }
return ScopedTreeRecord(
name=tr.name,
From 7c313d30af613ce82e4a0ca5cfebea412b2a4aa3 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Tue, 10 Sep 2024 10:01:57 -0500
Subject: [PATCH 70/78] misc
---
specifyweb/workbench/upload/treerecord.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 5595589a89c..6c57a182518 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -379,8 +379,8 @@ def check_root(root):
return result
# Determine the target treedef based on the columns that are not null
- targeted_treedefids = set([rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null])
- if targeted_treedefids is None or len(targeted_treedefids) == 0:
+ targeted_treedefids = {rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null}
+ if not targeted_treedefids:
# return self, WorkBenchParseFailure('noRanksInRow', {}, None)
return self, None
elif len(targeted_treedefids) > 1:
From e56e82c7fc17d362f986ff7f7751b052d908305f Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Tue, 10 Sep 2024 12:11:57 -0400
Subject: [PATCH 71/78] Move rank key functions to uploadPlanBuilder and
uploadPlanParser
---
.../components/WbPlanView/mappingHelpers.ts | 22 -------------------
.../WbPlanView/uploadPlanBuilder.ts | 15 ++++++++++++-
.../components/WbPlanView/uploadPlanParser.ts | 11 +++++++++-
3 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
index 401a9623288..dc65961182d 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingHelpers.ts
@@ -111,28 +111,6 @@ export const formatTreeDefinition = (definitionName: string): string =>
export const formatTreeRank = (rankName: string): string =>
`${schema.treeRankSymbol}${rankName}`;
-// Delimiter used for rank name keys i.e: ~>
-const RANK_KEY_DELIMITER = '~>';
-
-/**
- * Returns a formatted tree rank name along with its tree id: (e.x $Kingdom => $Kingdom~>1)
- * Used for generating unique key names when there are multiple trees with the same rank name.
- * Opposite of getRankNameWithoutTreeId
- * See: https://github.com/specify/specify7/pull/5091#issuecomment-2328037741
- */
-export const formatTreeRankWithTreeId = (
- rankName: string,
- treeId: number
-): string => `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
-
-/**
- * Returns the tree rank name after stripping its tree id: (e.x Kingdom~>1 => Kingdom)
- * NOTE: Does not consider whether rankName is formatted with $ or not.
- * Opposite of formatTreeRankWithTreeId
- */
-export const getRankNameWithoutTreeId = (rankName: string): string =>
- rankName.split(RANK_KEY_DELIMITER)[0];
-
// Match fields names like startDate_fullDate, but not _formatted
export const valueIsPartialField = (value: string): boolean =>
value.includes(schema.fieldPartSeparator) &&
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 0fbc2d45f3b..83c61811ba0 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -6,7 +6,6 @@ import type { Tables } from '../DataModel/types';
import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { SplitMappingPath } from './mappingHelpers';
-import { formatTreeRankWithTreeId } from './mappingHelpers';
import {
getNameFromTreeDefinitionName,
valueIsTreeDefinition,
@@ -212,3 +211,17 @@ const indexMappings = (
] as const
)
);
+
+// Delimiter used for rank name keys i.e: ~>
+export const RANK_KEY_DELIMITER = '~>';
+
+/**
+ * Returns a formatted tree rank name along with its tree id: (e.x Kingdom => Kingdom~>1)
+ * Used for generating unique key names in the upload plan when there are multiple trees with the same rank name.
+ * Opposite of uploadPlanParser.ts > getRankNameWithoutTreeId()
+ * See: https://github.com/specify/specify7/pull/5091#issuecomment-2328037741
+ */
+const formatTreeRankWithTreeId = (
+ rankName: string,
+ treeId: number
+): string => `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index e3e54e73127..718fb137b39 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -6,9 +6,10 @@ import { softFail } from '../Errors/Crash';
import { getTreeDefinitions } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { MappingPath } from './Mapper';
-import { getRankNameWithoutTreeId, SplitMappingPath } from './mappingHelpers';
+import { SplitMappingPath } from './mappingHelpers';
import { formatTreeDefinition } from './mappingHelpers';
import { formatToManyIndex, formatTreeRank } from './mappingHelpers';
+import { RANK_KEY_DELIMITER } from './uploadPlanBuilder';
export type MatchBehaviors = 'ignoreAlways' | 'ignoreNever' | 'ignoreWhenBlank';
@@ -212,3 +213,11 @@ export function parseUploadPlan(uploadPlan: UploadPlan): {
),
};
}
+
+/**
+ * Returns the tree rank name after stripping its tree id: (e.x Kingdom~>1 => Kingdom)
+ * NOTE: Does not consider whether rankName is formatted with $ or not.
+ * Opposite of uploadPlanBuilder.ts > formatTreeRankWithTreeId()
+ */
+const getRankNameWithoutTreeId = (rankName: string): string =>
+ rankName.split(RANK_KEY_DELIMITER)[0];
From 10324c97095b34c296b37d3a18a354dba7a95e6c Mon Sep 17 00:00:00 2001
From: Sharad S
Date: Tue, 10 Sep 2024 16:15:25 +0000
Subject: [PATCH 72/78] Lint code with ESLint and Prettier
Triggered by e56e82c7fc17d362f986ff7f7751b052d908305f on branch refs/heads/issue-4980
---
.../js_src/lib/components/WbPlanView/uploadPlanBuilder.ts | 6 ++----
.../js_src/lib/components/WbPlanView/uploadPlanParser.ts | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 83c61811ba0..1ffd74284af 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -221,7 +221,5 @@ export const RANK_KEY_DELIMITER = '~>';
* Opposite of uploadPlanParser.ts > getRankNameWithoutTreeId()
* See: https://github.com/specify/specify7/pull/5091#issuecomment-2328037741
*/
-const formatTreeRankWithTreeId = (
- rankName: string,
- treeId: number
-): string => `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
+const formatTreeRankWithTreeId = (rankName: string, treeId: number): string =>
+ `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
index 718fb137b39..f5220b43d2e 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanParser.ts
@@ -6,7 +6,7 @@ import { softFail } from '../Errors/Crash';
import { getTreeDefinitions } from '../InitialContext/treeRanks';
import { defaultColumnOptions } from './linesGetter';
import type { MappingPath } from './Mapper';
-import { SplitMappingPath } from './mappingHelpers';
+import type { SplitMappingPath } from './mappingHelpers';
import { formatTreeDefinition } from './mappingHelpers';
import { formatToManyIndex, formatTreeRank } from './mappingHelpers';
import { RANK_KEY_DELIMITER } from './uploadPlanBuilder';
From e8a9ed4b00fbdf883ac7192b5cfd0eef652c8e85 Mon Sep 17 00:00:00 2001
From: Caroline D <108160931+CarolineDenis@users.noreply.github.com>
Date: Wed, 11 Sep 2024 07:42:20 -0700
Subject: [PATCH 73/78] Add tree name in front of rank
---
.../components/WbPlanView/uploadPlanBuilder.ts | 15 ++++-----------
.../js_src/lib/tests/fixtures/uploadplan.1.json | 12 ++++++------
2 files changed, 10 insertions(+), 17 deletions(-)
diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
index 1ffd74284af..3082316a7e4 100644
--- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
+++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts
@@ -63,7 +63,7 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
? [[fullName, rankMappedFields]]
: indexMappings(rankMappedFields);
- const treeName = valueIsTreeDefinition(fullName)
+ const treeName: string | undefined = valueIsTreeDefinition(fullName)
? getNameFromTreeDefinitionName(fullName)
: undefined;
@@ -83,7 +83,9 @@ const toTreeRecordVariety = (lines: RA): TreeRecord => {
return [
treeId === undefined
? rankName
- : formatTreeRankWithTreeId(rankName, treeId),
+ : `${
+ treeName === undefined ? '' : treeName + RANK_KEY_DELIMITER
+ }${rankName}`,
{
treeNodeCols: toTreeRecordRanks(mappedFields),
treeId,
@@ -214,12 +216,3 @@ const indexMappings = (
// Delimiter used for rank name keys i.e: ~>
export const RANK_KEY_DELIMITER = '~>';
-
-/**
- * Returns a formatted tree rank name along with its tree id: (e.x Kingdom => Kingdom~>1)
- * Used for generating unique key names in the upload plan when there are multiple trees with the same rank name.
- * Opposite of uploadPlanParser.ts > getRankNameWithoutTreeId()
- * See: https://github.com/specify/specify7/pull/5091#issuecomment-2328037741
- */
-const formatTreeRankWithTreeId = (rankName: string, treeId: number): string =>
- `${rankName}${RANK_KEY_DELIMITER}${treeId}`;
diff --git a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
index 26476e857f6..a639bc1327d 100644
--- a/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
+++ b/specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
@@ -275,7 +275,7 @@
"taxon": {
"mustMatchTreeRecord": {
"ranks": {
- "Class~>1": {
+ "Class": {
"treeNodeCols": {
"name": {
"matchBehavior": "ignoreAlways",
@@ -286,32 +286,32 @@
},
"treeId": 1
},
- "Family~>1": {
+ "Family": {
"treeNodeCols": {
"name": "Family"
},
"treeId": 1
},
- "Genus~>1": {
+ "Genus": {
"treeNodeCols": {
"name": "Genus"
},
"treeId": 1
},
- "Subgenus~>1": {
+ "Subgenus": {
"treeNodeCols": {
"name": "Subgenus"
},
"treeId": 1
},
- "Species~>1": {
+ "Species": {
"treeNodeCols": {
"name": "Species",
"author": "Species Author"
},
"treeId": 1
},
- "Subspecies~>1": {
+ "Subspecies": {
"treeNodeCols": {
"name": "Subspecies",
"author": "Subspecies Author"
From e00add22bee6e62590340eae745a777f1f851750 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 11 Sep 2024 11:30:41 -0500
Subject: [PATCH 74/78] catch multipleRanksForTreedef WorkBenchParseFailure
---
specifyweb/workbench/upload/treerecord.py | 44 +++++++++++++++++------
1 file changed, 33 insertions(+), 11 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 6c57a182518..2934f5fcfff 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -2,9 +2,8 @@
For uploading tree records.
"""
-from collections import namedtuple
+from itertools import groupby
import logging
-import re
from typing import List, Dict, Any, Tuple, NamedTuple, Optional, Union, Set
from django.db import transaction, IntegrityError
@@ -265,7 +264,7 @@ def get_treedefs(self) -> Set:
tree_def_model = get_treedef_model(self.name)
treedefids = set([tree_rank_record.treedef_id for tree_rank_record in self.ranks.keys()])
return set(tree_def_model.objects.filter(id__in=treedefids))
-
+
def _get_not_null_ranks_columns_in_row(self, row: Row) -> List[TreeRankCell]:
"""
Get rank columns that are not null in the row.
@@ -308,7 +307,7 @@ def process_row_item(row_key: str, row_value: Any) -> List[TreeRankCell]:
for row_key, row_value in row.items()
for cell in process_row_item(row_key, row_value)
]
-
+
def rescope_tree_from_row(self, row: Row) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
"""Rescope tree from row data."""
@@ -328,12 +327,34 @@ def get_targeted_treedefids(ranks_columns: List[TreeRankCell]) -> Set[int]:
return set(rank_column.treedef_id for rank_column in ranks_columns)
# Handle cases where there are multiple or no treedefs
- def handle_multiple_or_no_treedefs(targeted_treedefids: Set[int], ranks_columns: List[TreeRankCell]):
+ def handle_multiple_or_no_treedefs(
+ targeted_treedefids: Set[int], ranks_columns: List[TreeRankCell]
+ ) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
if not targeted_treedefids:
return self, None
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
return self, WorkBenchParseFailure('multipleRanksInRow', {}, ranks_columns[0].treedefitem_name)
+
+ # Group ranks_columns by treedef_id using itertools.groupby
+ ranks_columns.sort(key=lambda x: x.treedef_id) # groupby requires sorted input
+ grouped_by_treedef_id = {k: list(v) for k, v in groupby(ranks_columns, key=lambda x: x.treedef_id)}
+
+ # Check if any treedef_id has more than one rank
+ multiple_ranks = any(len(columns) > 1 for columns in grouped_by_treedef_id.values())
+ if multiple_ranks:
+ treedef_id = next(
+ treedef_id
+ for treedef_id, columns in grouped_by_treedef_id.items()
+ if len(columns) > 1
+ )
+ logger.warning(f"Multiple ranks found for treedef_id {treedef_id}")
+ return self, WorkBenchParseFailure(
+ "multipleRanksForTreedef",
+ {},
+ grouped_by_treedef_id[treedef_id][0].treedefitem_name,
+ )
+
return None
# Retrieve the target rank treedef
@@ -360,7 +381,7 @@ def check_root(root):
treedefitems = fetch_treedefitems()
root = fetch_root()
root_checked = check_root(root)
-
+
if root_checked is not root:
return root_checked
@@ -368,9 +389,6 @@ def check_root(root):
tree_def_model, tree_rank_model, tree_node_model = get_models(self.name)
- if len(set(tr.treedef_id for tr in self.ranks.keys())) == 1:
- return self, None
-
ranks_columns_in_row_not_null = get_not_null_ranks_columns(row)
targeted_treedefids = get_targeted_treedefids(ranks_columns_in_row_not_null)
@@ -378,6 +396,10 @@ def check_root(root):
if result:
return result
+ unique_treedef_ids = {tr.treedef_id for tr in self.ranks.keys()}
+ if len(unique_treedef_ids) == 1:
+ return self, None
+
# Determine the target treedef based on the columns that are not null
targeted_treedefids = {rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null}
if not targeted_treedefids:
@@ -386,11 +408,11 @@ def check_root(root):
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0].treedefitem_name)
-
+
target_rank_treedef_id = targeted_treedefids.pop()
target_rank_treedef = get_target_rank_treedef(tree_def_model, target_rank_treedef_id)
- # Based on the target treedef, get the treedefitems and root for the tree
+ # Based on the target treedef, get the treedefitems and root for the tree
treedefitems = list(tree_rank_model.objects.filter(treedef_id=target_rank_treedef_id).order_by("rankid"))
root = tree_node_model.objects.filter(definition_id=target_rank_treedef_id, parent=None).first()
From 20bfd8af0f7308d332139e0a99d37f7718ec919d Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 11 Sep 2024 11:49:42 -0500
Subject: [PATCH 75/78] edit accepted formatting to treedef_id~>rank_name
---
specifyweb/workbench/upload/treerecord.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 2934f5fcfff..38513150d0c 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -135,12 +135,12 @@ def handle_multiple_items(treedefitems):
def extract_treedef_id(rank_name: str) -> Tuple[Union[str, Any], Optional[int]]:
"""
- Extract treedef_id from rank_name if it exists in the format 'rank_name~>treedef_id'.
+ Extract treedef_id from rank_name if it exists in the format 'treedef_id~>rank_name'.
"""
- parts = rank_name.rsplit('~>', 1)
- if len(parts) == 2 and parts[1].isdigit():
- rank_name = parts[0]
- tree_def_id = int(parts[1])
+ parts = rank_name.split('~>', 1)
+ if len(parts) == 2 and parts[0].isdigit():
+ tree_def_id = int(parts[0])
+ rank_name = parts[1]
return rank_name, tree_def_id
return rank_name, None
@@ -329,7 +329,7 @@ def get_targeted_treedefids(ranks_columns: List[TreeRankCell]) -> Set[int]:
# Handle cases where there are multiple or no treedefs
def handle_multiple_or_no_treedefs(
targeted_treedefids: Set[int], ranks_columns: List[TreeRankCell]
- ) -> Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]:
+ ) -> Optional[Tuple["ScopedTreeRecord", Optional["WorkBenchParseFailure"]]]:
if not targeted_treedefids:
return self, None
elif len(targeted_treedefids) > 1:
From 590579ea8ac1dfe399606eaeedaa1cf2fdb61bf1 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 11 Sep 2024 14:09:04 -0500
Subject: [PATCH 76/78] change extract_treedef_id to extract_treedef_name
---
specifyweb/workbench/upload/treerecord.py | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 38513150d0c..b8c4d8c0c7a 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -96,7 +96,8 @@ def validate_filtered_items(treedefitems):
def get_treedef_id(
rank_name: str,
tree: str,
- treedef_id: Optional[int],
+ treedef_name: Optional[str] = None,
+ treedef_id: Optional[int] = None
) -> int:
"""
Get the treedef ID for the given rank name and tree.
@@ -119,6 +120,9 @@ def handle_multiple_items(treedefitems):
return filter_by_treedef_id(treedefitems, rank_name, treedef_id)
return treedefitems
+ if treedef_id is None and treedef_name is not None:
+ treedef_id = get_treedef_model(tree).objects.get(name=treedef_name)
+
# Build filter keyword arguments and fetch treedefitems
filter_kwargs = build_filter_kwargs(rank_name, treedef_id)
treedefitems = fetch_treedefitems(filter_kwargs)
@@ -133,19 +137,19 @@ def handle_multiple_items(treedefitems):
return first_item.treedef_id
- def extract_treedef_id(rank_name: str) -> Tuple[Union[str, Any], Optional[int]]:
+ def extract_treedef_name(rank_name: str) -> Tuple[str, Optional[str]]:
"""
- Extract treedef_id from rank_name if it exists in the format 'treedef_id~>rank_name'.
+ Extract treedef_name from rank_name if it exists in the format 'treedef_name~>rank_name'.
"""
parts = rank_name.split('~>', 1)
- if len(parts) == 2 and parts[0].isdigit():
- tree_def_id = int(parts[0])
+ if len(parts) == 2:
+ treedef_name = parts[0]
rank_name = parts[1]
- return rank_name, tree_def_id
+ return rank_name, treedef_name
return rank_name, None
- rank_name, extracted_treedef_id = extract_treedef_id(rank_name)
- target_treedef_id = get_treedef_id(rank_name, tree, extracted_treedef_id or treedef_id)
+ rank_name, extracted_treedef_name = extract_treedef_name(rank_name)
+ target_treedef_id = get_treedef_id(rank_name, tree, extracted_treedef_name, treedef_id)
return TreeRank(rank_name, target_treedef_id, tree.lower())
From c3f4862cdf8c8b9ca3e9741bf7f482a818e5ae62 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 11 Sep 2024 14:13:22 -0500
Subject: [PATCH 77/78] avoid unit test failures, allow multiple ranks when
only one treedef is given
---
specifyweb/workbench/upload/treerecord.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index b8c4d8c0c7a..9beee830b31 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -393,6 +393,10 @@ def check_root(root):
tree_def_model, tree_rank_model, tree_node_model = get_models(self.name)
+ unique_treedef_ids = {tr.treedef_id for tr in self.ranks.keys()}
+ if len(unique_treedef_ids) == 1:
+ return self, None
+
ranks_columns_in_row_not_null = get_not_null_ranks_columns(row)
targeted_treedefids = get_targeted_treedefids(ranks_columns_in_row_not_null)
@@ -400,10 +404,6 @@ def check_root(root):
if result:
return result
- unique_treedef_ids = {tr.treedef_id for tr in self.ranks.keys()}
- if len(unique_treedef_ids) == 1:
- return self, None
-
# Determine the target treedef based on the columns that are not null
targeted_treedefids = {rank_column.treedef_id for rank_column in ranks_columns_in_row_not_null}
if not targeted_treedefids:
From eb3c8200bad92af9f43c063655ba47f8a02badf2 Mon Sep 17 00:00:00 2001
From: alec_dev
Date: Wed, 11 Sep 2024 15:05:52 -0500
Subject: [PATCH 78/78] fix WorkBenchParseFailure column name
---
specifyweb/workbench/upload/treerecord.py | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/specifyweb/workbench/upload/treerecord.py b/specifyweb/workbench/upload/treerecord.py
index 9beee830b31..92e29df7729 100644
--- a/specifyweb/workbench/upload/treerecord.py
+++ b/specifyweb/workbench/upload/treerecord.py
@@ -4,6 +4,8 @@
from itertools import groupby
import logging
+from math import e
+from os import error
from typing import List, Dict, Any, Tuple, NamedTuple, Optional, Union, Set
from django.db import transaction, IntegrityError
@@ -26,6 +28,7 @@ class TreeRankCell(NamedTuple):
treedefitem_name: str
tree_node_attribute: str
upload_value: str
+ column_fullname: str = ""
class TreeRank(NamedTuple):
rank_name: str
@@ -287,12 +290,13 @@ def match_column(row_key: str, formatted_column: str) -> bool:
return formatted_column == row_key
# Create a TreeRankCell instance
- def create_tree_rank_cell(tree_rank_record: Any, col_name: str, row_value: Any) -> TreeRankCell:
+ def create_tree_rank_cell(tree_rank_record: Any, col_name: str, row_value: Any, row_key: Any) -> TreeRankCell:
return TreeRankCell(
tree_rank_record.treedef_id, # treedefid
tree_rank_record.rank_name, # treedefitem_name
col_name, # tree_node_attribute
row_value, # upload_value
+ row_key # column_fullname
)
# Process the row item
@@ -300,7 +304,7 @@ def process_row_item(row_key: str, row_value: Any) -> List[TreeRankCell]:
if not is_not_null(row_value):
return []
return [
- create_tree_rank_cell(tree_rank_record, col_name, row_value)
+ create_tree_rank_cell(tree_rank_record, col_name, row_value, row_key)
for tree_rank_record, cols in self.ranks.items()
for col_name, col_opts in cols.items()
if match_column(row_key, format_column(col_name, col_opts))
@@ -338,7 +342,8 @@ def handle_multiple_or_no_treedefs(
return self, None
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, ranks_columns[0].treedefitem_name)
+ error_col_name = ranks_columns[0].column_fullname
+ return self, WorkBenchParseFailure('multipleRanksInRow', {}, error_col_name)
# Group ranks_columns by treedef_id using itertools.groupby
ranks_columns.sort(key=lambda x: x.treedef_id) # groupby requires sorted input
@@ -353,11 +358,8 @@ def handle_multiple_or_no_treedefs(
if len(columns) > 1
)
logger.warning(f"Multiple ranks found for treedef_id {treedef_id}")
- return self, WorkBenchParseFailure(
- "multipleRanksForTreedef",
- {},
- grouped_by_treedef_id[treedef_id][0].treedefitem_name,
- )
+ error_col_name = grouped_by_treedef_id[treedef_id][0].column_fullname
+ return self, WorkBenchParseFailure("multipleRanksForTreedef", {}, error_col_name)
return None
@@ -411,7 +413,8 @@ def check_root(root):
return self, None
elif len(targeted_treedefids) > 1:
logger.warning(f"Multiple treedefs found in row: {targeted_treedefids}")
- return self, WorkBenchParseFailure('multipleRanksInRow', {}, list(ranks_columns_in_row_not_null)[0].treedefitem_name)
+ error_col_name = list(ranks_columns_in_row_not_null)[0].column_fullname
+ return self, WorkBenchParseFailure("multipleRanksInRow", {}, error_col_name)
target_rank_treedef_id = targeted_treedefids.pop()
target_rank_treedef = get_target_rank_treedef(tree_def_model, target_rank_treedef_id)