Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
const loadedResults = (
undefinedResult === -1 ? results : results?.slice(0, undefinedResult)
) as RA<QueryResultRow> | undefined;

/* eslint-disable functional/prefer-readonly-type */
const deletingRef = React.useRef<Set<number>>(new Set()); // Track recent deleted IDs to prevent duplicate deletion
const deletingRef = React.useRef<Set<number>>(new Set()); // Track recent deleted IDs to prevent duplicate deletion

// TEST: try deleting while records are being fetched
/**
Expand All @@ -161,7 +161,7 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
(recordId: number): void => {
if (deletingRef.current.has(recordId)) return; // Prevents duplicate deletion calls for the same record
deletingRef.current.add(recordId);

let removeCount = 0;
function newResults(results: RA<QueryResultRow | undefined> | undefined) {
if (!Array.isArray(results) || totalCount === undefined) return;
Expand All @@ -179,7 +179,9 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
return;
}
setTotalCount((totalCount) =>
totalCount === undefined ? undefined : Math.max(0, totalCount - removeCount)
totalCount === undefined
? undefined
: Math.max(0, totalCount - removeCount)
);
const newSelectedRows = (selectedRows: ReadonlySet<number>) =>
new Set(Array.from(selectedRows).filter((id) => id !== recordId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export function ImportTree<SCHEMA extends AnyTree>({
setSelectedPopulatedTree(resource);
// Check for missing ranks if no preference for createMissingRanks was provided.
if (createMissingRanks === undefined) {
console.log('finding if theres missing ranks');
try {
const response = await ajax<any>(`/trees/default_tree_mapping/`, {
method: 'POST',
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.")
66 changes: 65 additions & 1 deletion specifyweb/specify/migration_utils/update_schema_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.db import connection, transaction
from django.db.models.functions import RowNumber

from specifyweb.specify.models_utils.load_datamodel import Table, FieldDoesNotExistError, TableDoesNotExistError
from specifyweb.specify.models_utils.load_datamodel import FieldDoesNotExistError, TableDoesNotExistError
from specifyweb.specify.models_utils.model_extras import GEOLOGY_DISCIPLINES, PALEO_DISCIPLINES
from specifyweb.specify.models import (
Discipline,
Expand Down Expand Up @@ -567,6 +567,70 @@ def update_table_field_schema_config_params(
setattr(sp_local_container_item, k, v)
sp_local_container_item.save(update_fields=list(update_params.keys()))

def find_missing_schema_config_fields(discipline_id: int, apps=global_apps):
Splocalecontainer = apps.get_model('specify', 'Splocalecontainer')
Splocalecontaineritem = apps.get_model('specify', 'Splocalecontaineritem')

missing_tables: list[str] = []
missing_fields: dict[str, list[str]] = {}

containers = Splocalecontainer.objects.filter(
discipline_id=discipline_id,
schematype=0,
)
container_names = set(
containers.values_list('name', flat=True)
)

existing_fields_by_table: dict[str, set[str]] = defaultdict(set)
for table_name, field_name in Splocalecontaineritem.objects.filter(
container__in=containers
).values_list('container__name', 'name'):
if table_name and field_name:
existing_fields_by_table[table_name].add(field_name.lower())

for table in datamodel.tables:
table_name = table.name
table_name_lower = table_name.lower()
if table_name_lower not in container_names:
missing_tables.append(table_name)
missing_fields[table_name] = sorted(
field.name for field in table.all_fields if field.name
)
continue

existing_fields = existing_fields_by_table.get(table_name_lower, set())
missing_in_table = sorted( # sort for better reproducablity
field.name
for field in table.all_fields
if field.name and field.name.lower() not in existing_fields
)

if missing_in_table:
missing_fields[table_name] = missing_in_table

return missing_tables, missing_fields


def create_missing_schema_config_fields(discipline_id: int, apps=global_apps, stdout=None):
missing_tables, missing_fields = find_missing_schema_config_fields(discipline_id, apps=apps)
missing_table_set = set(missing_tables)

for table_name in missing_tables:
if stdout is not None:
stdout(f"Creating schema config table container for {table_name}...")
update_table_schema_config_with_defaults(table_name, discipline_id, apps=apps)

for table_name, fields in missing_fields.items():
if table_name in missing_table_set:
continue
for field_name in fields:
if stdout is not None:
stdout(f"Creating schema config field {table_name}.{field_name}...")
update_table_field_schema_config_with_defaults(table_name, discipline_id, field_name, apps=apps)

return missing_tables, missing_fields

def deduplicate_schema_config_sql(apps=None):
dedupe_sql = '''
/*
Expand Down