Skip to content
Draft
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
50 changes: 34 additions & 16 deletions specifyweb/frontend/js_src/lib/components/SetupTool/SetupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import type {
} from '../TreeView/CreateTree';
import { PopulatedTreeList } from '../TreeView/CreateTree';
import type { FieldConfig, ResourceConfig } from './setupResources';
import { disciplineTypeOptions, FIELD_MAX_LENGTH, resources,stepOrder } from './setupResources';
import {
disciplineTypeOptions,
FIELD_MAX_LENGTH,
resources,
stepOrder,
} from './setupResources';
import type { ResourceFormData } from './types';

function getFormValue(
Expand Down Expand Up @@ -142,15 +147,20 @@ export function renderFormFieldFactory({

const verticalSpacing = width !== undefined && width < 2 ? '-mb-2' : 'mb-2';

const disciplineTypeValue = getFormValue(formData, stepOrder.indexOf('discipline'), 'type');
const disciplineTypeValue = getFormValue(
formData,
stepOrder.indexOf('discipline'),
'type'
);
const isDisciplineNameDisabled =
resources[currentStep].resourceName === 'discipline' &&
fieldName === 'name' &&
(disciplineTypeValue === undefined || disciplineTypeValue === '');

const isRowEnableToggle = name === 'include';
const isRowEnabled =
Boolean(getFormValue(formData, currentStep, `${parentName}.include`));
const isRowEnabled = Boolean(
getFormValue(formData, currentStep, `${parentName}.include`)
);

const taxonTreeAvailable =
Array.isArray(treeOptions) &&
Expand All @@ -177,9 +187,7 @@ export function renderFormFieldFactory({
getFormValue(formData, currentStep, fieldName)
)}
disabled={
inTable
? !isRowEnabled && !isRowEnableToggle
: false
inTable ? !isRowEnabled && !isRowEnableToggle : false
}
id={fieldName}
name={fieldName}
Expand Down Expand Up @@ -325,7 +333,7 @@ export function renderFormFieldFactory({
<PopulatedTreeList
discipline={disciplineTypeValue as string | undefined}
handleClick={(
resource: TaxonFileDefaultDefinition,
resource: TaxonFileDefaultDefinition
): void => {
handleChange(fieldName, resource);
handleChange('preload', true);
Expand All @@ -351,9 +359,14 @@ export function renderFormFieldFactory({
value={getFormValue(formData, currentStep, fieldName) ?? ''}
onChange={({ target }) => {
// Only allow unique discipline names
if (resources[currentStep].resourceName === 'discipline' && fieldName === 'name') {
if (
resources[currentStep].resourceName === 'discipline' &&
fieldName === 'name'
) {
const value = (target.value ?? '').trim();
const isUnique = institutionData.children.some((child) => child.name === value);
const isUnique = institutionData.children.some(
(child) => child.name === value
);
target.setCustomValidity(
isUnique ? '' : formsText.valueMustBeUniqueToDatabase()
);
Expand Down Expand Up @@ -438,7 +451,7 @@ export function updateSetupFormData(
name: string,
newValue: LocalizedString | TaxonFileDefaultDefinition | boolean,
currentStep: number,
institutionData?: InstitutionData,
institutionData?: InstitutionData
): void {
setFormData((previous: ResourceFormData) => {
const resourceName = resources[currentStep].resourceName;
Expand All @@ -454,10 +467,15 @@ export function updateSetupFormData(
(option) => option.value === newValue
);
if (matchingType) {
const existingDisciplines = institutionData ? institutionData.children.flatMap(division =>
division.children.map(discipline => discipline.name)
) : [];
const disciplineName = getUniqueName(matchingType.label, existingDisciplines);
const existingDisciplines = institutionData
? institutionData.children.flatMap((division) =>
division.children.map((discipline) => discipline.name)
)
: [];
const disciplineName = getUniqueName(
matchingType.label,
existingDisciplines
);
updates.name = matchingType ? disciplineName : '';
}

Expand All @@ -482,4 +500,4 @@ export function updateSetupFormData(
[resourceName]: updates,
};
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export type SetupResponse = {
readonly success: boolean;
readonly setup_progress: SetupProgress;
readonly task_id: string;
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,9 @@ export function Hierarchy({
{addButton(
// Use custom forms for discipline to allow tree configuration
() => {
setFormData(Object.fromEntries(stepOrder.map((key) => [key, {}])));
setFormData(
Object.fromEntries(stepOrder.map((key) => [key, {}]))
);
openDisciplineCreation();
setDisciplineStep(0);
},
Expand Down
61 changes: 31 additions & 30 deletions specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';

import { commonText } from '../../localization/common';
import { treeText } from '../../localization/tree';
import { queryText } from '../../localization/query';
import { treeText } from '../../localization/tree';
import { ajax } from '../../utils/ajax';
import { Http } from '../../utils/ajax/definitions';
import { ping } from '../../utils/ajax/ping';
Expand Down Expand Up @@ -200,8 +200,8 @@ export function ImportTree<SCHEMA extends AnyTree>({
readonly tableName: SCHEMA['tableName'];
readonly treeDefId: number;
readonly treeDefinitionItems: RA<
SerializedResource<FilterTablesByEndsWith<'TreeDefItem'>>
>;
SerializedResource<FilterTablesByEndsWith<'TreeDefItem'>>
>;
}): JSX.Element {
const loading = React.useContext(LoadingContext);
const [isActive, setIsActive] = React.useState(0);
Expand All @@ -211,9 +211,14 @@ export function ImportTree<SCHEMA extends AnyTree>({
string | undefined
>(undefined);

const [missingTreeRanks, setMissingTreeRanks] = React.useState<RA<string> | undefined>(undefined);
const [isMissingTreeRanks, setIsMissingTreeRanks] = React.useState<boolean>(false);
const [selectedPopulatedTree, setSelectedPopulatedTree] = React.useState<TaxonFileDefaultDefinition | undefined>(undefined);
const [missingTreeRanks, setMissingTreeRanks] = React.useState<
RA<string> | undefined
>(undefined);
const [isMissingTreeRanks, setIsMissingTreeRanks] =
React.useState<boolean>(false);
const [selectedPopulatedTree, setSelectedPopulatedTree] = React.useState<
TaxonFileDefaultDefinition | undefined
>(undefined);

const connectedCollection = getSystemInfo().collection;

Expand All @@ -225,21 +230,26 @@ export function ImportTree<SCHEMA extends AnyTree>({
// Check for missing ranks if no preference for createMissingRanks was provided.
console.log(createMissingRanks);
if (createMissingRanks === undefined) {
console.log("finding if theres missing ranks");
console.log('finding if theres missing ranks');
try {
const response = await ajax<any>(`/trees/default_tree_mapping/`, {
method: 'POST',
headers: { Accept: 'application/json' },
body: {
mappingUrl: resource.mappingFile
}
mappingUrl: resource.mappingFile,
},
});
if (response.status === Http.OK && response.data) {
const mappingRankNames = response.data.ranks.map((rank: any) => {return rank.name});
const existingNames = treeDefinitionItems.map((item) => item.name);
const mappingRankNames = response.data.ranks.map(
(rank: any) => rank.name
);
const existingNames = new Set(
treeDefinitionItems.map((item) => item.name)
);

const missing = mappingRankNames
.filter((rankName: string) => !existingNames.includes(rankName));
const missing = mappingRankNames.filter(
(rankName: string) => !existingNames.has(rankName)
);

if (missing.length > 0) {
setSelectedPopulatedTree(resource);
Expand All @@ -251,8 +261,8 @@ export function ImportTree<SCHEMA extends AnyTree>({
} else {
console.warn(`Failed to fetch mapping for ${resource.mappingFile}`);
}
} catch (err) {
console.warn('Error fetching or parsing mapping file', err);
} catch (error) {
console.warn('Error fetching or parsing mapping file', error);
}
}

Expand Down Expand Up @@ -291,10 +301,10 @@ export function ImportTree<SCHEMA extends AnyTree>({
setIsMissingTreeRanks(false);
handleClick(selectedPopulatedTree, true);
}}
missingTreeRanks={missingTreeRanks}
onClose={() => {
setIsMissingTreeRanks(false);
}}
missingTreeRanks={missingTreeRanks}
/>
) : null}
{isActive === 1 ? (
Expand Down Expand Up @@ -345,7 +355,7 @@ async function startTreeCreation(
treeName: string,
treeDefId: number | undefined,
createMissingRanks: boolean | undefined,
onSuccess: (taskId: string | undefined) => void,
onSuccess: (taskId: string | undefined) => void
): Promise<void> {
return ajax<TreeCreationInfo>('/trees/create_default_tree/', {
method: 'POST',
Expand All @@ -366,10 +376,7 @@ async function startTreeCreation(
console.log(`${treeName} tree created successfully:`, data);
} else if (status === Http.ACCEPTED) {
// Tree is being created in the background.
console.log(
`${treeName} tree creation started successfully:`,
data
);
console.log(`${treeName} tree creation started successfully:`, data);
onSuccess(data.task_id);
}
})
Expand Down Expand Up @@ -471,28 +478,22 @@ export function MissingTreeRanksDialog({
<Button.Secondary onClick={handleNo}>
{commonText.no()}
</Button.Secondary>
<Button.Info onClick={handleYes}>
{queryText.yes()}
</Button.Info>
<Button.Info onClick={handleYes}>{queryText.yes()}</Button.Info>
</>
}
header={treeText.missingRanks()}
onClose={onClose}
>
<div className="mb-4 flex flex-col gap-4">
<section>
{treeText.missingRanksDescription()}
</section>
<section>{treeText.missingRanksDescription()}</section>
<section>
<ul className="ml-4">
{missingTreeRanks && missingTreeRanks.length > 0
? missingTreeRanks.map((rank) => <li key={rank}>{rank}</li>)
: null}
</ul>
</section>
<section>
{treeText.createMissingRanks()}
</section>
<section>{treeText.createMissingRanks()}</section>
</div>
</Dialog>
);
Expand Down
6 changes: 5 additions & 1 deletion specifyweb/frontend/js_src/lib/components/TreeView/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ export function Tree<
onClick={createRootNode}
/>
{treeDefId ? (
<ImportTree tableName={tableName} treeDefId={treeDefId} treeDefinitionItems={treeDefinitionItems}/>
<ImportTree
tableName={tableName}
treeDefId={treeDefId}
treeDefinitionItems={treeDefinitionItems}
/>
) : null}
</div>
) : undefined}
Expand Down
3 changes: 2 additions & 1 deletion specifyweb/frontend/js_src/lib/localization/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,8 @@ export const treeText = createDictionary({
'en-us': 'Missing Ranks',
},
missingRanksDescription: {
'en-us': 'The populated tree you selected to download contains records in ranks that are missing from your tree.',
'en-us':
'The populated tree you selected to download contains records in ranks that are missing from your tree.',
},
createMissingRanks: {
'en-us': 'Should the missing ranks be created?',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from django.core.management.base import BaseCommand, CommandError
from django.apps import apps

from specifyweb.specify.migration_utils import update_schema_config as update_schema


class Command(BaseCommand):
help = "Finds missing schema config fields for a discipline and can create them."

def add_arguments(self, parser):
parser.add_argument(
"--discipline-id",
type=int,
dest="discipline_id",
required=True,
help="Discipline ID to target.",
)
parser.add_argument(
"--apply",
action="store_true",
dest="apply",
default=False,
help="Create any missing schema config records.",
)
parser.add_argument(
"--verbose",
action="store_true",
dest="verbose",
default=False,
help="Print each create operation as it runs.",
)

def handle(self, **options):
discipline_id = options.get("discipline_id")
apply_changes = options.get("apply", False)
verbose = options.get("verbose", False)

# Resolve the discipline by given ID
Discipline = apps.get_model("specify", "Discipline")
try:
discipline = Discipline.objects.get(id=discipline_id)
except Discipline.DoesNotExist as exc:
raise CommandError(
f"Discipline with ID {discipline_id} not found."
) from exc

self.stdout.write(
f"Discipline: {discipline.name} (ID={discipline.id})"
)

missing_tables, missing_fields = update_schema.find_missing_schema_config_fields(discipline.id, apps=apps,)

if not missing_tables and not missing_fields:
self.stdout.write("No missing schema config fields found.")
return

# Print out what would be created if applied.
if missing_tables:
self.stdout.write("Missing table containers:")
for table_name in sorted(missing_tables):
self.stdout.write(f"- {table_name}")

if missing_fields:
self.stdout.write("Missing fields:")
for table_name in sorted(missing_fields.keys()):
field_names = missing_fields[table_name]
if not field_names:
continue
joined_fields = ", ".join(field_names)
self.stdout.write(f"- {table_name}: {joined_fields}")

if not apply_changes:
self.stdout.write("Run again with --apply to create missing records.")
return

# Apply changes
update_schema.create_missing_schema_config_fields(
discipline.id,
apps=apps,
stdout=self.stdout.write if verbose else None,
)
self.stdout.write("Applied missing schema config records.")
Loading