From dad1d5c4be8c352e6cecfc052e101cf4e916b29b Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 10:40:56 +0200 Subject: [PATCH 01/25] Fix: Use db_alias for migration db connexion --- specifyweb/backend/patches/migration_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/specifyweb/backend/patches/migration_utils.py b/specifyweb/backend/patches/migration_utils.py index a773cb10ad0..4dfdc463387 100644 --- a/specifyweb/backend/patches/migration_utils.py +++ b/specifyweb/backend/patches/migration_utils.py @@ -10,6 +10,7 @@ def apply_migrations(app_registry, schema_editor=None): update_coordinates(app_registry, schema_editor) def update_is_accepted(app_registry, schema_editor=None): + db_alias = schema_editor.connection.alias if schema_editor is not None else "default" for tree in SPECIFY_TREES: tree_filters = { "isaccepted": False, @@ -17,20 +18,21 @@ def update_is_accepted(app_registry, schema_editor=None): } tree_model = app_registry.get_model("specify", tree) - tree_model.objects.filter(**tree_filters).update(isaccepted=True) + tree_model._base_manager.using(db_alias).filter(**tree_filters).update(isaccepted=True) def update_coordinates(app_registry, schema_editor=None): + db_alias = schema_editor.connection.alias if schema_editor is not None else "default" Locality = app_registry.get_model("specify", "Locality") - Locality.objects.filter(lat1text__isnull=True, latitude1__isnull=False) \ + Locality._base_manager.using(db_alias).filter(lat1text__isnull=True, latitude1__isnull=False) \ .update(lat1text=F("latitude1")) - Locality.objects.filter(long1text__isnull=True, longitude1__isnull=False) \ + Locality._base_manager.using(db_alias).filter(long1text__isnull=True, longitude1__isnull=False) \ .update(long1text=F("longitude1")) - Locality.objects.filter(lat2text__isnull=True, latitude2__isnull=False) \ + Locality._base_manager.using(db_alias).filter(lat2text__isnull=True, latitude2__isnull=False) \ .update(lat2text=F("latitude2")) - Locality.objects.filter(long2text__isnull=True, longitude2__isnull=False) \ + Locality._base_manager.using(db_alias).filter(long2text__isnull=True, longitude2__isnull=False) \ .update(long2text=F("longitude2")) From 65dffca4e10c12a03b936860ac6c05325416db9f Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 10:43:31 +0200 Subject: [PATCH 02/25] Fix: flips existing row back to isDatabaseConstraint=True --- specifyweb/backend/businessrules/migration_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/businessrules/migration_utils.py b/specifyweb/backend/businessrules/migration_utils.py index 38265a6b20c..f5d689b9853 100644 --- a/specifyweb/backend/businessrules/migration_utils.py +++ b/specifyweb/backend/businessrules/migration_utils.py @@ -1,4 +1,4 @@ -from typing import Tuple, List +from typing import List from specifyweb.backend.businessrules.uniqueness_rules import create_uniqueness_rule @@ -47,6 +47,7 @@ def catnum_rule_uneditable(apps, schema_editor=None): model_rules = UniquenessRule.objects.filter(modelName="Collectionobject", discipline_id=discipline.id, isDatabaseConstraint=False) has_catalognumber_rule = False + matching_rule_ids: List[int] = [] for rule in model_rules: rule_fields = rule.uniquenessrulefield_set.all() @@ -59,8 +60,11 @@ def catnum_rule_uneditable(apps, schema_editor=None): # exception if more than one result is returned if (len(fields) == 1 and len(scopes) == 1) and (fields.get().fieldPath.lower() == "catalognumber" and scopes.get().fieldPath.lower() == "collection"): has_catalognumber_rule = True + matching_rule_ids.append(rule.id) - if not has_catalognumber_rule: + if has_catalognumber_rule: + UniquenessRule.objects.filter(id__in=matching_rule_ids).update(isDatabaseConstraint=True) + else: create_uniqueness_rule( "Collectionobject", discipline=discipline, From 9691f60e5c6e249610bb86a2bd8b8ce1d3b4e088 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 10:52:05 +0200 Subject: [PATCH 03/25] Fix: compare full rule definitions before deleting anything in uniqueness rule --- .../backend/businessrules/uniqueness_rules.py | 92 +++++++++---------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/specifyweb/backend/businessrules/uniqueness_rules.py b/specifyweb/backend/businessrules/uniqueness_rules.py index 52e5b8e360b..0f06851b821 100644 --- a/specifyweb/backend/businessrules/uniqueness_rules.py +++ b/specifyweb/backend/businessrules/uniqueness_rules.py @@ -362,12 +362,19 @@ def create_uniqueness_rule(model_name, raw_discipline, is_database_constraint, f isDatabaseConstraint=is_database_constraint, discipline=discipline) + expected_fields = set(fields) + expected_scopes = set(scopes) + for rule in candidate_rules: all_fields = rule.uniquenessrulefield_set.all() - matching_fields = all_fields.filter(fieldPath__in=fields, isScope=False) - matching_scopes = all_fields.filter(fieldPath__in=scopes, isScope=True) + existing_fields = set( + all_fields.filter(isScope=False).values_list("fieldPath", flat=True) + ) + existing_scopes = set( + all_fields.filter(isScope=True).values_list("fieldPath", flat=True) + ) # If the rule already exists, skip creating the rule - if len(matching_fields) == len(fields) and len(matching_scopes) == len(scopes): + if existing_fields == expected_fields and existing_scopes == expected_scopes: return logger.info(f"Creating uniqueness rule on {model_name} with fields {fields} and scopes {scopes} for the discipline {discipline.name if discipline else 'Global'}") @@ -393,13 +400,20 @@ def remove_uniqueness_rule(model_name, raw_discipline, is_database_constraint, f candidate_rules = UniquenessRule.objects.filter( modelName=model_name, isDatabaseConstraint=is_database_constraint, discipline=discipline) + expected_fields = set(fields) + expected_scopes = set(scopes) + rule_ids = [] for rule in candidate_rules: all_fields = rule.uniquenessrulefield_set.all() - matching_fields = all_fields.filter(fieldPath__in=fields, isScope=False) - matching_scopes = all_fields.filter(fieldPath__in=scopes, isScope=True) + existing_fields = set( + all_fields.filter(isScope=False).values_list("fieldPath", flat=True) + ) + existing_scopes = set( + all_fields.filter(isScope=True).values_list("fieldPath", flat=True) + ) # If the rule exists, add it to the list of rules to be deleted - if len(matching_fields) == len(fields) and len(matching_scopes) == len(scopes): + if existing_fields == expected_fields and existing_scopes == expected_scopes: rule_ids.append(rule.id) UniquenessRuleField.objects.filter( @@ -421,24 +435,18 @@ def fix_global_default_rules(registry=None): UniquenessRule = registry.get_model('businessrules', 'UniquenessRule') \ if registry \ else models.UniquenessRule - UniquenessRuleField = registry.get_model('businessrules', 'UniquenessRuleField') \ - if registry \ - else models.UniquenessRuleField - - global_rule_fields = UniquenessRuleField.objects.filter( - uniquenessrule__discipline__isnull=True - ).values( - "uniquenessrule__modelName", - "uniquenessrule__isDatabaseConstraint", - "fieldPath", - "isScope", - ) - - global_rule_exists = UniquenessRule.objects.filter( - discipline__isnull=True, - modelName=OuterRef("modelName"), - isDatabaseConstraint=OuterRef("isDatabaseConstraint"), - ) + global_rule_signatures = { + ( + rule.modelName, + rule.isDatabaseConstraint, + frozenset( + rule.uniquenessrulefield_set.values_list("fieldPath", "isScope") + ), + ) + for rule in UniquenessRule.objects.filter( + discipline__isnull=True + ).prefetch_related("uniquenessrulefield_set") + } discipline_ids = ( UniquenessRule.objects.exclude(discipline__isnull=True) @@ -448,28 +456,16 @@ def fix_global_default_rules(registry=None): for discipline_id in discipline_ids: with transaction.atomic(): - # Delete matching fields for this discipline - matching_fields_qs = UniquenessRuleField.objects.filter( - uniquenessrule__discipline_id=discipline_id - ).filter( - Exists( - global_rule_fields.filter( - **{ - "uniquenessrule__modelName": OuterRef("uniquenessrule__modelName"), - "uniquenessrule__isDatabaseConstraint": OuterRef("uniquenessrule__isDatabaseConstraint"), - "fieldPath": OuterRef("fieldPath"), - "isScope": OuterRef("isScope"), - } - ) + for rule in UniquenessRule.objects.filter( + discipline_id=discipline_id + ).prefetch_related("uniquenessrulefield_set"): + signature = ( + rule.modelName, + rule.isDatabaseConstraint, + frozenset( + rule.uniquenessrulefield_set.values_list("fieldPath", "isScope") + ), ) - ) - matching_fields_qs.delete() - - # Delete UniquenessRule rows for this discipline that are now empty - empty_rules_qs = ( - UniquenessRule.objects.filter(discipline_id=discipline_id) - .annotate(field_count=Count("uniquenessrulefield")) - .filter(field_count=0) # now empty after field deletions - .filter(Exists(global_rule_exists)) - ) - empty_rules_qs.delete() + if signature in global_rule_signatures: + rule.uniquenessrulefield_set.all().delete() + rule.delete() From 72511773990580c2acdb611f481e3723a5bfbb66 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 10:56:35 +0200 Subject: [PATCH 04/25] Fix: verify existing user permissions --- specifyweb/backend/permissions/initialize.py | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/specifyweb/backend/permissions/initialize.py b/specifyweb/backend/permissions/initialize.py index a9cf2c62d5b..ac5e2cc3c94 100644 --- a/specifyweb/backend/permissions/initialize.py +++ b/specifyweb/backend/permissions/initialize.py @@ -53,13 +53,14 @@ def create_admins(apps=apps) -> None: UserPolicy = apps.get_model('permissions', 'UserPolicy') Specifyuser = apps.get_model('specify', 'Specifyuser') - if UserPolicy.objects.filter(collection__isnull=True, resource='%', action='%').exists(): - # don't do anything if there is already any admin. - return - users = Specifyuser.objects.all() for user in users: - if is_sp6_user_permissions_migrated(user, apps): + if UserPolicy.objects.filter( + collection__isnull=True, + specifyuser_id=user.id, + resource="%", + action="%", + ).exists(): continue if is_legacy_admin(user): UserPolicy.objects.get_or_create( @@ -112,17 +113,12 @@ def assign_users_to_roles(apps=apps) -> None: JOIN spprincipal p ON p.SpPrincipalID = up.SpPrincipalID JOIN collection c ON c.UserGroupScopeId = p.userGroupScopeID WHERE p.groupType IS NULL - AND u.SpecifyUserID NOT IN ( - SELECT ur.specifyuser_id + AND NOT EXISTS ( + SELECT 1 FROM spuserrole ur JOIN sprole r ON r.id = ur.role_id - WHERE r.collection_id = p.usergroupscopeid - ) - AND c.UserGroupScopeId NOT IN ( - SELECT DISTINCT r.collection_id - FROM spuserrole ur - JOIN sprole r ON r.id = ur.role_id - JOIN collection c ON c.UserGroupScopeId = r.collection_id + WHERE r.collection_id = c.UserGroupScopeId + AND ur.specifyuser_id = u.SpecifyUserID ); """) From 6df417bd8076cde79ed9fe40d8b66736fcbf8cde Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:05:34 +0200 Subject: [PATCH 05/25] Fix: Remove the use of f-string --- .../management/commands/run_key_migration_functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/specify/management/commands/run_key_migration_functions.py b/specifyweb/specify/management/commands/run_key_migration_functions.py index 33d679b7957..0cdea30691a 100644 --- a/specifyweb/specify/management/commands/run_key_migration_functions.py +++ b/specifyweb/specify/management/commands/run_key_migration_functions.py @@ -214,7 +214,7 @@ def add_arguments(self, parser): nargs="*", type=str, choices=tuple(self.funcs.keys()), - help=f"Optional: specify one or more functions to run", + help="Optional: specify one or more functions to run", ) parser.add_argument( "--verbose", @@ -247,6 +247,6 @@ def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS(f"Applying {func_name}...")) func(self.stdout.write if verbose else None) self.stdout.write(self.style.SUCCESS(f"Applied {func_name}")) - except Exception as e: - logger.error(f"An error occurred: {e}") + except Exception: + logger.exception("An error occurred while running key migrations") raise From 8950f946a9fc300ab7497a2e5055c9467efca626 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:07:32 +0200 Subject: [PATCH 06/25] fix: prevent incorrect reuse when pairing tree defs and disciplines --- .../specify/migration_utils/default_cots.py | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/specifyweb/specify/migration_utils/default_cots.py b/specifyweb/specify/migration_utils/default_cots.py index 000b7572053..68f771b51c3 100644 --- a/specifyweb/specify/migration_utils/default_cots.py +++ b/specifyweb/specify/migration_utils/default_cots.py @@ -136,22 +136,25 @@ def fix_tectonic_unit_treedef_discipline_links(apps): Discipline = apps.get_model('specify', 'Discipline') Tectonicunittreedef = apps.get_model('specify', 'Tectonicunittreedef') - empty_tectonic_unit_treedefs = Tectonicunittreedef.objects.filter(discipline__isnull=True) - empty_disciplines = Discipline.objects.filter(tectonicunittreedef__isnull=True) - for empty_discipline in empty_disciplines: - if not empty_tectonic_unit_treedefs.exists(): - new_tectonic_unit_treedef = Tectonicunittreedef.objects.create( - name=f'{empty_discipline.name} Tectonic Unit Tree', - discipline=empty_discipline - ) - else: - empty_discipline.tectonicunittreedef = empty_tectonic_unit_treedefs.first() - empty_discipline.save() - - for empty_tectonic_unit_treedef in empty_tectonic_unit_treedefs: - if empty_disciplines.exists(): - empty_tectonic_unit_treedef.discipline = empty_disciplines.first() - empty_tectonic_unit_treedef.save() - else: - empty_tectonic_unit_treedef.discipline = empty_disciplines.last() - empty_tectonic_unit_treedef.save() + empty_tectonic_unit_treedefs = list( + Tectonicunittreedef.objects.filter(discipline__isnull=True) + ) + empty_disciplines = list( + Discipline.objects.filter(tectonicunittreedef__isnull=True) + ) + + for discipline, tectonic_unit_treedef in zip( + empty_disciplines, empty_tectonic_unit_treedefs + ): + tectonic_unit_treedef.discipline = discipline + tectonic_unit_treedef.save() + discipline.tectonicunittreedef = tectonic_unit_treedef + discipline.save() + + for discipline in empty_disciplines[len(empty_tectonic_unit_treedefs):]: + tectonic_unit_treedef = Tectonicunittreedef.objects.create( + name=f'{discipline.name} Tectonic Unit Tree', + discipline=discipline + ) + discipline.tectonicunittreedef = tectonic_unit_treedef + discipline.save() \ No newline at end of file From 7ba9e3554289db276b424102022635ce1923df21 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:11:47 +0200 Subject: [PATCH 07/25] fix: remove exclusion so partially migrated disciplines are repaired correctly --- .../specify/migration_utils/tectonic_ranks.py | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/specifyweb/specify/migration_utils/tectonic_ranks.py b/specifyweb/specify/migration_utils/tectonic_ranks.py index c2afe88c6fa..e05207e2755 100644 --- a/specifyweb/specify/migration_utils/tectonic_ranks.py +++ b/specifyweb/specify/migration_utils/tectonic_ranks.py @@ -6,14 +6,13 @@ def create_default_tectonic_ranks(apps): TectonicTreeDef = apps.get_model('specify', 'TectonicUnitTreeDef') Discipline = apps.get_model('specify', 'Discipline') - disciplines = Discipline.objects.filter(tectonicunittreedef__isnull=True).exclude( - id__in=TectonicTreeDef.objects.values_list('discipline_id', flat=True) - ) + disciplines = Discipline.objects.filter(tectonicunittreedef__isnull=True) for discipline in disciplines: - tectonic_tree_def = TectonicTreeDef.objects.filter(discipline=discipline).first() - if not tectonic_tree_def: - tectonic_tree_def, _ = TectonicTreeDef.objects.get_or_create(name="Tectonic Unit", discipline=discipline) + tectonic_tree_def, _ = TectonicTreeDef.objects.get_or_create( + name="Tectonic Unit", + discipline=discipline, + ) root, _ = TectonicUnitTreeDefItem.objects.get_or_create( name="Root", @@ -93,16 +92,26 @@ def create_root_tectonic_node(apps): tectonic_tree_def = TectonicUnitTreeDef.objects.filter(name="Tectonic Unit", discipline=discipline).first() if not tectonic_tree_def: - tectonic_tree_def, is_created = TectonicUnitTreeDef.objects.get_or_create( + tectonic_tree_def, _ = TectonicUnitTreeDef.objects.get_or_create( name="Tectonic Unit", discipline=discipline ) - tectonic_tree_def_item = TectonicUnitTreeDefItem.objects.filter(treedef=tectonic_tree_def, name="Root").first() - if not tectonic_tree_def_item: - tectonic_tree_def_item, is_created = TectonicUnitTreeDefItem.objects.get_or_create( + tectonic_tree_def_item = TectonicUnitTreeDefItem.objects.filter( + treedef=tectonic_tree_def, + name="Root", + ).first() + if tectonic_tree_def_item: + tectonic_tree_def_item.rankid = 0 + tectonic_tree_def_item.parent = None + tectonic_tree_def_item.isenforced = True + tectonic_tree_def_item.save() + else: + tectonic_tree_def_item, _ = TectonicUnitTreeDefItem.objects.get_or_create( name="Root", title="Root", + rankid=0, + parent=None, treedef=tectonic_tree_def, isenforced=True ) @@ -135,7 +144,9 @@ def revert_create_root_tectonic_node(apps, schema_editor=None): tectonic_tree_def = TectonicTreeDef.objects.filter(name="Tectonic Unit", discipline=discipline).first() if tectonic_tree_def: - TectonicUnitTreeDefItem.objects.filter(treedef=tectonic_tree_def).delete() TectonicUnit.objects.filter( - name="Root" - ).delete() \ No newline at end of file + name="Root", + definition=tectonic_tree_def, + parent__isnull=True, + ).delete() + TectonicUnitTreeDefItem.objects.filter(treedef=tectonic_tree_def).delete() \ No newline at end of file From c6c82687b398c174c0e30ba4cfa88a2cd96d8a3c Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:22:18 +0200 Subject: [PATCH 08/25] Fix: Update splocalecontainer items --- .../migration_utils/update_schema_config.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/specifyweb/specify/migration_utils/update_schema_config.py b/specifyweb/specify/migration_utils/update_schema_config.py index 5d5e2b89f84..504f0b008bf 100644 --- a/specifyweb/specify/migration_utils/update_schema_config.py +++ b/specifyweb/specify/migration_utils/update_schema_config.py @@ -1025,7 +1025,7 @@ def update_cog_type_fields(apps): container_items = Splocalecontaineritem.objects.filter( name="collectionObjectType", picklistname=None, - container__name="CollectionObject", + container__name="Collectionobject", ) for container_item in container_items: Splocaleitemstr.objects.filter(itemname=container_item).delete() @@ -1437,7 +1437,7 @@ def update_schema_config_field_desc(apps, schema_editor=None): #i.e: COType items = Splocalecontaineritem.objects.filter( container=container, - name=field_name.lower() + name__iexact=field_name ) for item in items: @@ -1639,7 +1639,7 @@ def update_schema_config_field_desc(apps): #i.e: COType items = Splocalecontaineritem.objects.filter( container=container, - name=field_name.lower() + name__iexact=field_name ) for item in items: @@ -1715,7 +1715,7 @@ def update_schema_config_field_desc(apps): #i.e: COType items = Splocalecontaineritem.objects.filter( container=container, - name=field_name.lower() + name__iexact=field_name ) for item in items: @@ -1841,7 +1841,7 @@ def update_schema_config_field_desc(apps): #i.e: COType items = Splocalecontaineritem.objects.filter( container=container, - name=field_name.lower() + name__iexact=field_name ) for item in items: @@ -1934,19 +1934,19 @@ def update_0034_schema_config_field_desc(apps): for (field_name, new_name, new_desc) in fields: items = Splocalecontaineritem.objects.filter( container=container, - name=field_name.lower() + name__iexact=field_name ) for item in items: item.ishidden = True item.save() desc_str = Splocaleitemstr.objects.filter(itemdesc_id=item.id).first() name_str = Splocaleitemstr.objects.filter(itemname_id=item.id).first() - if not desc_str or not name_str: - continue - desc_str.text = new_desc - desc_str.save() - name_str.text = new_name - name_str.save() + if desc_str is not None: + desc_str.text = new_desc + desc_str.save() + if name_str is not None: + name_str.text = new_name + name_str.save() update_0034_fields(apps) update_0034_schema_config_field_desc(apps) From 1fe82b2747278134018051a1816c02276b3beaab Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:23:31 +0200 Subject: [PATCH 09/25] TODO --- specifyweb/specify/migration_utils/update_schema_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/specify/migration_utils/update_schema_config.py b/specifyweb/specify/migration_utils/update_schema_config.py index 504f0b008bf..051c0acf34f 100644 --- a/specifyweb/specify/migration_utils/update_schema_config.py +++ b/specifyweb/specify/migration_utils/update_schema_config.py @@ -1937,6 +1937,7 @@ def update_0034_schema_config_field_desc(apps): name__iexact=field_name ) for item in items: + # TODO: is this correct? item.ishidden = True item.save() desc_str = Splocaleitemstr.objects.filter(itemdesc_id=item.id).first() From 6b6e6c8cf05fb836e85c66a2ba707aac1b070962 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:24:44 +0200 Subject: [PATCH 10/25] Fix: Remove shadowing import in geo migration --- specifyweb/specify/migrations/0002_geo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/specify/migrations/0002_geo.py b/specifyweb/specify/migrations/0002_geo.py index 58cffd3a12f..b0a523d4316 100644 --- a/specifyweb/specify/migrations/0002_geo.py +++ b/specifyweb/specify/migrations/0002_geo.py @@ -16,7 +16,6 @@ create_default_discipline_for_tree_defs, set_discipline_for_taxon_treedefs, ) -from specifyweb.specify.api.utils import create_default_collection_types logger = logging.getLogger(__name__) From 8c4343fe2bf40b161d42f96a7bb8065329f4b044 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:25:57 +0200 Subject: [PATCH 11/25] Fix: Revert relative age migration instead of applying again --- specifyweb/specify/migrations/0008_ageCitations_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/specify/migrations/0008_ageCitations_fix.py b/specifyweb/specify/migrations/0008_ageCitations_fix.py index 479d9dae005..9f058e2dbf3 100644 --- a/specifyweb/specify/migrations/0008_ageCitations_fix.py +++ b/specifyweb/specify/migrations/0008_ageCitations_fix.py @@ -15,7 +15,7 @@ def apply_migration(apps, schema_editor): usc.update_relative_age_fields(apps) def revert_migration(apps, schema_editor): - usc.update_relative_age_fields(apps) + usc.revert_relative_age_fields(apps) operations = [ migrations.AddField( From ee58b9520a553da294c11374699ead4919f7712e Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:27:08 +0200 Subject: [PATCH 12/25] Fix fix order of revert migration in tectonic migration --- specifyweb/specify/migrations/0009_tectonic_ranks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/specify/migrations/0009_tectonic_ranks.py b/specifyweb/specify/migrations/0009_tectonic_ranks.py index 54182060ebe..f97c7560a19 100644 --- a/specifyweb/specify/migrations/0009_tectonic_ranks.py +++ b/specifyweb/specify/migrations/0009_tectonic_ranks.py @@ -18,8 +18,8 @@ def consolidated_python_django_migration_operations(apps, schema_editor): create_root_tectonic_node(apps) def revert_cosolidated_python_django_migration_operations(apps, schema_editor): - revert_default_tectonic_ranks(apps, schema_editor) revert_create_root_tectonic_node(apps, schema_editor) + revert_default_tectonic_ranks(apps, schema_editor) operations = [ migrations.RunPython( From 7632deefa25b48644a665536098fb55b08a01765 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:31:29 +0200 Subject: [PATCH 13/25] fix(migration): make 0027_CO_children self-contained and idempotent --- .../specify/migrations/0027_CO_children.py | 158 +++++++++++++++++- 1 file changed, 154 insertions(+), 4 deletions(-) diff --git a/specifyweb/specify/migrations/0027_CO_children.py b/specifyweb/specify/migrations/0027_CO_children.py index 0dc7ccadad1..b30d8dd4896 100644 --- a/specifyweb/specify/migrations/0027_CO_children.py +++ b/specifyweb/specify/migrations/0027_CO_children.py @@ -1,8 +1,158 @@ # Generated by Django 3.2.15 on 2025-04-11 15:35 -from django.apps import apps as specify_apps +from django.core.exceptions import MultipleObjectsReturned from django.db import migrations, models import django.db.models.deletion -from specifyweb.specify.migration_utils import update_schema_config as usc +from django.db.models import Q +from specifyweb.specify.models import datamodel +from specifyweb.specify.models_utils.load_datamodel import FieldDoesNotExistError + + +MIGRATION_0027_FIELDS = { + 'CollectionObject': ['parentCO', 'children'], +} + +MIGRATION_0027_UPDATE_FIELDS = { + 'CollectionObject': [ + ('parentCO', 'Parent Collection Object', 'Parent CollectionObject'), + ('children', 'Children', 'Children'), + ] +} + +HIDDEN_FIELDS = [ + "timestampcreated", "timestampmodified", "version", "createdbyagent", "modifiedbyagent" +] + + +def datamodel_type_to_schematype(datamodel_type: str) -> str: + return "".join(map(lambda type_part: type_part.lower().capitalize(), datamodel_type.split('-'))) + + +def camel_to_spaced_title_case(camel_case: str) -> str: + return re.sub(r"(? Date: Wed, 20 May 2026 11:55:33 +0200 Subject: [PATCH 14/25] Fix: Indentation --- specifyweb/backend/businessrules/uniqueness_rules.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/backend/businessrules/uniqueness_rules.py b/specifyweb/backend/businessrules/uniqueness_rules.py index 0f06851b821..9718e62ccdd 100644 --- a/specifyweb/backend/businessrules/uniqueness_rules.py +++ b/specifyweb/backend/businessrules/uniqueness_rules.py @@ -466,6 +466,6 @@ def fix_global_default_rules(registry=None): rule.uniquenessrulefield_set.values_list("fieldPath", "isScope") ), ) - if signature in global_rule_signatures: - rule.uniquenessrulefield_set.all().delete() - rule.delete() + if signature in global_rule_signatures: + rule.uniquenessrulefield_set.all().delete() + rule.delete() From 594a04d7df0b9512d65d6c2ad446cadf2680069f Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:57:04 +0200 Subject: [PATCH 15/25] Fix: Use deterministic ordering before positional pairing --- specifyweb/specify/migration_utils/default_cots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/specify/migration_utils/default_cots.py b/specifyweb/specify/migration_utils/default_cots.py index 68f771b51c3..8f7bd2d2843 100644 --- a/specifyweb/specify/migration_utils/default_cots.py +++ b/specifyweb/specify/migration_utils/default_cots.py @@ -137,10 +137,10 @@ def fix_tectonic_unit_treedef_discipline_links(apps): Tectonicunittreedef = apps.get_model('specify', 'Tectonicunittreedef') empty_tectonic_unit_treedefs = list( - Tectonicunittreedef.objects.filter(discipline__isnull=True) + Tectonicunittreedef.objects.filter(discipline__isnull=True).order_by('id') ) empty_disciplines = list( - Discipline.objects.filter(tectonicunittreedef__isnull=True) + Discipline.objects.filter(tectonicunittreedef__isnull=True).order_by('id') ) for discipline, tectonic_unit_treedef in zip( From c459bead4dd45d3657c47188cfc585774b399cd2 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:57:38 +0200 Subject: [PATCH 16/25] Fix: Add import --- specifyweb/specify/migrations/0027_CO_children.py | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/specify/migrations/0027_CO_children.py b/specifyweb/specify/migrations/0027_CO_children.py index b30d8dd4896..cb03ce2959d 100644 --- a/specifyweb/specify/migrations/0027_CO_children.py +++ b/specifyweb/specify/migrations/0027_CO_children.py @@ -1,4 +1,5 @@ # Generated by Django 3.2.15 on 2025-04-11 15:35 +import re from django.core.exceptions import MultipleObjectsReturned from django.db import migrations, models import django.db.models.deletion From 30b1d518819582dfd56f5c2dadb1efd964269619 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 20 May 2026 11:59:19 +0200 Subject: [PATCH 17/25] Fix: Improve lookup --- specifyweb/specify/migrations/0027_CO_children.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/specifyweb/specify/migrations/0027_CO_children.py b/specifyweb/specify/migrations/0027_CO_children.py index cb03ce2959d..3c7251f0a14 100644 --- a/specifyweb/specify/migrations/0027_CO_children.py +++ b/specifyweb/specify/migrations/0027_CO_children.py @@ -70,11 +70,13 @@ def update_table_field_schema_config_with_defaults( sp_local_container_item, _ = Splocalecontaineritem.objects.get_or_create( name=field_name, container=sp_local_container, - type=java_type, - ishidden=field_name.lower() in HIDDEN_FIELDS, - isrequired=field.required, - issystem=table.system, - version=0, + defaults={ + 'type': java_type, + 'ishidden': field_name.lower() in HIDDEN_FIELDS, + 'isrequired': field.required, + 'issystem': table.system, + 'version': 0, + } ) field_description = camel_to_spaced_title_case(field.name) From f847d7b1ec499901a5ec38c8b5d669e1613b37ad Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Thu, 21 May 2026 14:00:50 +0200 Subject: [PATCH 18/25] Fix: Log only for debug --- specifyweb/backend/stored_queries/execution.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/stored_queries/execution.py b/specifyweb/backend/stored_queries/execution.py index e20f4f8b61d..2b5b26dcdb4 100644 --- a/specifyweb/backend/stored_queries/execution.py +++ b/specifyweb/backend/stored_queries/execution.py @@ -878,7 +878,8 @@ def execute( - log_sqlalchemy_query(query) # Debugging + if settings.DEBUG: + log_sqlalchemy_query(query) return {"results": apply_special_post_query_processing(query, tableid, field_specs, collection, user)} def build_query( @@ -1097,7 +1098,8 @@ def series_post_query(query, limit=40, offset=0, sort_type=0, co_id_cat_num_pair and adding a co_id colum and formatted catnum range column. Sort the results by the first catnum in the range.""" - log_sqlalchemy_query(query) # Debugging + if settings.DEBUG: + log_sqlalchemy_query(query) def parse_catalog_for_comparing(s): def check_for_decimal(s): From 2408d2833aef10ed094689a3551ccda590167959 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Thu, 21 May 2026 14:07:44 +0200 Subject: [PATCH 19/25] Fix: Fix age type --- specifyweb/specify/migration_utils/sp7_schemaconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/specify/migration_utils/sp7_schemaconfig.py b/specifyweb/specify/migration_utils/sp7_schemaconfig.py index 7e4fce0c947..d7925ed0f51 100644 --- a/specifyweb/specify/migration_utils/sp7_schemaconfig.py +++ b/specifyweb/specify/migration_utils/sp7_schemaconfig.py @@ -148,7 +148,7 @@ 'RelativeAge': ['number2', 'yesno2', 'relativeAgeId', 'relativeAgePeriod', 'text1', 'agent1', 'collectionDate', 'text2', 'agent2', 'date1', 'date2', 'collectionObject', 'relativeAgeCitations', 'number1', 'yesno1'], 'CollectionObject': ['collectionObjectType', 'relativeAges', 'absoluteAges', 'cojo'], 'AbsoluteAgeCitation': ['collectionMember', 'absoluteAgeCitationId'], - 'RelativeAgeCitation': ['absoluteAgeCitationId', 'collectionMember'], + 'RelativeAgeCitation': ['relativeAgeCitationId', 'collectionMember'], 'TectonicUnit': ['collectionMember', 'nodeNumber', 'yesno1', 'tectonicUnitId', 'number1', 'yesno2', 'number2', 'rankId', 'text1'], 'TectonicUnitTreeDefItem': ['children', 'rankId', 'parent', 'treeDef', 'treeEntries', 'tectonicUnitTreeDefItemId'], 'TectonicUnitTreeDef': ['discipline', 'treeEntries', 'tectonicUnitTreeDefId'] From 12d9efaa3cf2e6222a84f31d41fc599e266f708e Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Thu, 21 May 2026 14:27:22 +0200 Subject: [PATCH 20/25] Fix: Improve logger --- specifyweb/specify/api/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/specifyweb/specify/api/utils.py b/specifyweb/specify/api/utils.py index 1b53ff42bd7..493ed0d6d5c 100644 --- a/specifyweb/specify/api/utils.py +++ b/specifyweb/specify/api/utils.py @@ -1,5 +1,6 @@ import logging +from specifyweb import settings from specifyweb.specify import models as spmodels from specifyweb.backend.businessrules.exceptions import BusinessRuleException @@ -19,13 +20,16 @@ def get_spmodel_class(model_name: str): raise AttributeError(f"Model '{model_name}' not found in models module.") def log_sqlalchemy_query(query): - # Call this function to debug the raw SQL query generated by SQLAlchemy + if not settings.DEBUG: + return + from sqlalchemy.dialects import mysql - compiled_query = query.statement.compile(dialect=mysql.dialect(), compile_kwargs={"literal_binds": True}) + compiled_query = query.statement.compile(dialect=mysql.dialect()) raw_sql = str(compiled_query).replace('\n', ' ') + ';' - logger.debug('='.join(['' for _ in range(80)])) - logger.debug(raw_sql) - logger.debug('='.join(['' for _ in range(80)])) + logger.debug("%s", "=" * 80) + logger.debug("SQL: %s", raw_sql) + logger.debug("Params: %s", compiled_query.params) + logger.debug("%s", "=" * 80) # Run in the storred_queries.execute file, in the execute function, right before the return statement, line 546 # from specifyweb.specify.utils import log_sqlalchemy_query; log_sqlalchemy_query(query) From 5329e1e7dfabfca035d5fdac4bf71d03e55cb6b3 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Fri, 22 May 2026 09:52:38 +0200 Subject: [PATCH 21/25] Fix: Revert chnages in 0027 migration --- .../specify/migrations/0027_CO_children.py | 158 +----------------- 1 file changed, 3 insertions(+), 155 deletions(-) diff --git a/specifyweb/specify/migrations/0027_CO_children.py b/specifyweb/specify/migrations/0027_CO_children.py index 3c7251f0a14..710382fd027 100644 --- a/specifyweb/specify/migrations/0027_CO_children.py +++ b/specifyweb/specify/migrations/0027_CO_children.py @@ -1,161 +1,9 @@ # Generated by Django 3.2.15 on 2025-04-11 15:35 import re -from django.core.exceptions import MultipleObjectsReturned from django.db import migrations, models import django.db.models.deletion from django.db.models import Q -from specifyweb.specify.models import datamodel -from specifyweb.specify.models_utils.load_datamodel import FieldDoesNotExistError - - -MIGRATION_0027_FIELDS = { - 'CollectionObject': ['parentCO', 'children'], -} - -MIGRATION_0027_UPDATE_FIELDS = { - 'CollectionObject': [ - ('parentCO', 'Parent Collection Object', 'Parent CollectionObject'), - ('children', 'Children', 'Children'), - ] -} - -HIDDEN_FIELDS = [ - "timestampcreated", "timestampmodified", "version", "createdbyagent", "modifiedbyagent" -] - - -def datamodel_type_to_schematype(datamodel_type: str) -> str: - return "".join(map(lambda type_part: type_part.lower().capitalize(), datamodel_type.split('-'))) - - -def camel_to_spaced_title_case(camel_case: str) -> str: - return re.sub(r"(? Date: Fri, 22 May 2026 09:54:22 +0200 Subject: [PATCH 22/25] Fix: Remove imports --- specifyweb/specify/migrations/0027_CO_children.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/specifyweb/specify/migrations/0027_CO_children.py b/specifyweb/specify/migrations/0027_CO_children.py index 710382fd027..2d17612f1ab 100644 --- a/specifyweb/specify/migrations/0027_CO_children.py +++ b/specifyweb/specify/migrations/0027_CO_children.py @@ -1,8 +1,6 @@ # Generated by Django 3.2.15 on 2025-04-11 15:35 -import re from django.db import migrations, models import django.db.models.deletion -from django.db.models import Q from specifyweb.specify.migration_utils import update_schema_config as usc class Migration(migrations.Migration): From 43af0577a5421b816e830679dc12d58717fd778c Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Fri, 22 May 2026 10:03:31 +0200 Subject: [PATCH 23/25] Fix: Remove unecessary param in def migration --- specifyweb/specify/migrations/0002_geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/specify/migrations/0002_geo.py b/specifyweb/specify/migrations/0002_geo.py index b0a523d4316..a3c32bc4b2c 100644 --- a/specifyweb/specify/migrations/0002_geo.py +++ b/specifyweb/specify/migrations/0002_geo.py @@ -72,7 +72,7 @@ class Migration(migrations.Migration): def consolidated_python_django_migration_operations(apps, schema_editor): db_alias = schema_editor.connection.alias or 'migrator' - create_default_collection_types(apps, using=db_alias) + create_default_collection_types(apps) create_default_discipline_for_tree_defs(apps, using=db_alias) usc.create_geo_table_schema_config_with_defaults(apps) create_cogtype_type_picklist(apps, using=db_alias) From c711878547869a675a3e6f091881d0e62fd99b52 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Fri, 22 May 2026 10:29:56 +0200 Subject: [PATCH 24/25] Fix: Update failing test for legacy project --- specifyweb/backend/stored_queries/tests/tests_legacy.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/backend/stored_queries/tests/tests_legacy.py b/specifyweb/backend/stored_queries/tests/tests_legacy.py index cb34b27ae22..1090b60f30b 100644 --- a/specifyweb/backend/stored_queries/tests/tests_legacy.py +++ b/specifyweb/backend/stored_queries/tests/tests_legacy.py @@ -841,9 +841,6 @@ def test_sqlalchemy_model_errors(self): ] }, "CollectionObject": { - "not_found": [ - "projects" - ], "incorrect_direction": { "cojo": [ "onetomany", From 875a23a9ef983f9778ea193eccbdd1870b210322 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 22 May 2026 09:55:18 -0500 Subject: [PATCH 25/25] Fix settings import in query logging helper --- specifyweb/specify/api/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/specify/api/utils.py b/specifyweb/specify/api/utils.py index 493ed0d6d5c..28615e70f17 100644 --- a/specifyweb/specify/api/utils.py +++ b/specifyweb/specify/api/utils.py @@ -1,6 +1,6 @@ import logging -from specifyweb import settings +from django.conf import settings from specifyweb.specify import models as spmodels from specifyweb.backend.businessrules.exceptions import BusinessRuleException @@ -94,4 +94,4 @@ def get_picklists(collection: spmodels.Collection, tablename: str, fieldname: st if len(collection_picklists) > 0: picklists = collection_picklists - return picklists, schemaitem \ No newline at end of file + return picklists, schemaitem