Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor TreeRankRecord to allow null tree id #6322

Merged
merged 16 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,27 @@ export function InteractionDialog({
})),
});

const [collectionHasSeveralTypes] = useAsyncState(
React.useCallback(
async () =>
fetchDomain.then(async (schema) => Object.keys(schema.collectionObjectTypeCatalogNumberFormats).length > 1),
[]
),
false
);
const [collectionHasSeveralTypes] = useAsyncState(
React.useCallback(
async () =>
fetchDomain.then(
async (schema) =>
Object.keys(schema.collectionObjectTypeCatalogNumberFormats)
.length > 1
),
[]
),
false
);

function handleParse(): RA<string> | undefined {
const parseResults = split(catalogNumbers).map((value) =>
parseValue(parser, inputRef.current ?? undefined, value)
);

const errorMessages = parseResults
.filter((result): result is InvalidParseResult => !result.isValid)
.map(({ reason, value }) => `${reason} (${value})`);
.filter((result): result is InvalidParseResult => !result.isValid)
.map(({ reason, value }) => `${reason} (${value})`);

if (errorMessages.length > 0 && collectionHasSeveralTypes === false) {
setValidation(errorMessages);
Expand All @@ -233,10 +237,10 @@ export function InteractionDialog({

if (collectionHasSeveralTypes === true) {
const parsedCatNumber = split(catalogNumbers);

setCatalogNumbers(parsedCatNumber.join('\n'));
setState({ type: 'MainState' });

return parsedCatNumber.map(String);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IR, RA, RR } from '../../utils/types';
import { group, removeKey, split, toLowerCase } from '../../utils/utils';
import type { AnyTree } from '../DataModel/helperTypes';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
Expand Down Expand Up @@ -47,8 +48,11 @@ const toTreeRecordRanks = (
])
);

const toTreeRecordVariety = (lines: RA<SplitMappingPath>): TreeRecord => {
const treeDefinitions = getTreeDefinitions('Taxon', 'all');
const toTreeRecordVariety = (
tableName: AnyTree['tableName'],
lines: RA<SplitMappingPath>
): TreeRecord => {
const treeDefinitions = getTreeDefinitions(tableName, 'all');

return {
ranks: Object.fromEntries(
Expand Down Expand Up @@ -87,7 +91,7 @@ const toTreeRecordVariety = (lines: RA<SplitMappingPath>): TreeRecord => {
: formatTreeRankKey(rankName, treeName),
{
treeNodeCols: toTreeRecordRanks(mappedFields),
treeId,
treeId: treeDefinitions.length > 1 ? treeId : undefined,
},
];
});
Expand Down Expand Up @@ -166,7 +170,7 @@ const toUploadable = (
mustMatchPreferences.includes(table.name)
? 'mustMatchTreeRecord'
: 'treeRecord',
toTreeRecordVariety(lines),
toTreeRecordVariety(table.name, lines),
] as const,
])
: Object.fromEntries([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IR, RA, RR } from '../../utils/types';
import type { AnyTree } from '../DataModel/helperTypes';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { strictGetTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
Expand Down Expand Up @@ -80,16 +81,16 @@ const parseTree = (
...mappingPath,
...(typeof rankData === 'object' &&
typeof rankData.treeId === 'number' &&
getTreeDefinitions('Taxon', 'all').length > 1
? [resolveTreeId(rankData.treeId)]
getTreeDefinitions(table.name as 'Geography', 'all').length > 1
? [resolveTreeId(table.name as 'Geography', rankData.treeId)]
: []),
formatTreeRank(getRankNameFromKey(rankName)),
]
)
);

const resolveTreeId = (id: number): string => {
const treeDefinition = getTreeDefinitions('Taxon', id);
const resolveTreeId = (tableName: AnyTree['tableName'], id: number): string => {
const treeDefinition = getTreeDefinitions(tableName, id);
return formatTreeDefinition(treeDefinition[0].definition.name);
};

Expand Down
18 changes: 6 additions & 12 deletions specifyweb/frontend/js_src/lib/tests/fixtures/uploadplan.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,40 +283,34 @@
"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
}
}
}
}
Expand Down
39 changes: 38 additions & 1 deletion specifyweb/specify/tree_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Tuple, List
from django.db.models import Q, Count, Model

import specifyweb.specify.models as spmodels
from specifyweb.specify.datamodel import datamodel
from specifyweb.specify.datamodel import datamodel, Table

lookup = lambda tree: (tree.lower() + 'treedef')

Expand Down Expand Up @@ -48,3 +49,39 @@ def get_treedefs(collection: spmodels.Collection, tree_name: str) -> List[Tuple

return result

def get_default_treedef(table: Table, collection):
if table.name.lower() not in SPECIFY_TREES:
raise Exception(f"unexpected tree type: {table.name}")

if table.name == 'Taxon':
return collection.discipline.taxontreedef

elif table.name == "Geography":
return collection.discipline.geographytreedef

elif table.name == "LithoStrat":
return collection.discipline.lithostrattreedef

elif table.name == "GeologicTimePeriod":
return collection.discipline.geologictimeperiodtreedef

elif table.name == "Storage":
return collection.discipline.division.institution.storagetreedef

elif table.name == 'TectonicUnit':
return collection.discipline.tectonicunittreedef

return None

def get_treedefitem_model(tree: str):
return getattr(spmodels, tree.lower().title() + 'treedefitem')

def get_treedef_model(tree: str):
return getattr(spmodels, tree.lower().title() + 'treedef')

def get_models(name: str):
tree_def_model = get_treedef_model(name)
tree_rank_model = get_treedefitem_model(name)
tree_node_model = getattr(spmodels, name.lower().title())

return tree_def_model, tree_rank_model, tree_node_model
31 changes: 7 additions & 24 deletions specifyweb/workbench/upload/scoping.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from specifyweb.specify.func import CustomRepr
from specifyweb.specify.load_datamodel import DoesNotExistError
from specifyweb.specify import models
from specifyweb.specify.tree_utils import get_default_treedef
from specifyweb.specify.uiformatters import get_uiformatter, get_catalognumber_format, UIFormatter
from specifyweb.specify.utils import get_picklists
from specifyweb.stored_queries.format import get_date_format
Expand Down Expand Up @@ -347,37 +348,15 @@ def set_order_number(
def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
table = datamodel.get_table_strict(tr.name)

treedef = None
if table.name == 'Taxon':
if treedef is None:
treedef = collection.discipline.taxontreedef

elif table.name == "Geography":
treedef = collection.discipline.geographytreedef

elif table.name == "LithoStrat":
treedef = collection.discipline.lithostrattreedef

elif table.name == "GeologicTimePeriod":
treedef = collection.discipline.geologictimeperiodtreedef

elif table.name == "Storage":
treedef = collection.discipline.division.institution.storagetreedef

elif table.name == 'TectonicUnit':
treedef = collection.discipline.tectonicunittreedef

else:
raise Exception(f"unexpected tree type: {table.name}")

treedef = get_default_treedef(table, collection)
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:
is_valid_rank = (hasattr(rank, 'rank_name') and rank.rank_name in treedef_ranks) or (rank in treedef_ranks) # type: ignore
if not is_valid_rank and isinstance(rank, TreeRankRecord) and not rank.check_rank(table.name):
if not is_valid_rank and (isinstance(rank, TreeRankRecord) and not rank.validate_rank(tr.name)):
raise Exception(f'"{rank}" not among {table.name} tree ranks: {treedef_ranks}')

root = list(
Expand All @@ -392,6 +371,7 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
r, table.name, treedef.id if treedef else None
).tree_rank_record()
if isinstance(r, str)
else r._replace(treedef_id=treedef.id) if r.treedef_id is None # Adjust treeid for parsed JSON plans
else r
): {
f: extend_columnoptions(colopts, collection, table.name, f)
Expand All @@ -400,6 +380,8 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
for r, cols in tr.ranks.items()
}

scoped_cotypes = models.Collectionobjecttype.objects.filter(collection=collection)

return ScopedTreeRecord(
name=tr.name,
ranks=scoped_ranks,
Expand All @@ -408,4 +390,5 @@ def apply_scoping_to_treerecord(tr: TreeRecord, collection) -> ScopedTreeRecord:
root=root[0] if root else None,
disambiguation={},
batch_edit_pack=None,
scoped_cotypes=scoped_cotypes
)
Loading
Loading