diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx index d46aa5b10ce..e9124f832c4 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx @@ -149,9 +149,9 @@ export function QueryResults(props: QueryResultsProps): JSX.Element { const loadedResults = ( undefinedResult === -1 ? results : results?.slice(0, undefinedResult) ) as RA | undefined; - + /* eslint-disable functional/prefer-readonly-type */ - const deletingRef = React.useRef>(new Set()); // Track recent deleted IDs to prevent duplicate deletion + const deletingRef = React.useRef>(new Set()); // Track recent deleted IDs to prevent duplicate deletion // TEST: try deleting while records are being fetched /** @@ -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 | undefined) { if (!Array.isArray(results) || totalCount === undefined) return; @@ -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) => new Set(Array.from(selectedRows).filter((id) => id !== recordId)); diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx index ca214d44a11..fdca10ead6f 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/CreateTree.tsx @@ -242,6 +242,7 @@ export function ImportTree({ 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(`/trees/default_tree_mapping/`, { method: 'POST', diff --git a/specifyweb/specify/management/commands/sync_schema_config_fields.py b/specifyweb/specify/management/commands/sync_schema_config_fields.py new file mode 100644 index 00000000000..5e01671d961 --- /dev/null +++ b/specifyweb/specify/management/commands/sync_schema_config_fields.py @@ -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.") diff --git a/specifyweb/specify/migration_utils/update_schema_config.py b/specifyweb/specify/migration_utils/update_schema_config.py index bf0e271dacf..960f7bf4c3d 100644 --- a/specifyweb/specify/migration_utils/update_schema_config.py +++ b/specifyweb/specify/migration_utils/update_schema_config.py @@ -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, @@ -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 = ''' /*