browsertrix/backend/btrixcloud/migrations/migration_0032_dupe_org_names.py
Tessa Walsh 6ccaad26d8
Ensure org name and slug uniqueness is case-insensitive (#1929)
Fixes #1927 

Also adds tests to ensure index is working as expected, and migration to
rename orgs that have names or slugs identical to other orgs except for
case before the new case-insensitive index is built.
2024-07-18 15:30:12 -07:00

95 lines
2.6 KiB
Python

"""
Migration 0032 - Case-insensitive org name duplicates
"""
from uuid import UUID
from pymongo.errors import DuplicateKeyError
from btrixcloud.migrations import BaseMigration
from btrixcloud.utils import slug_from_name
MIGRATION_VERSION = "0032"
class Migration(BaseMigration):
"""Migration class."""
# pylint: disable=unused-argument
def __init__(self, mdb, **kwargs):
super().__init__(mdb, migration_version=MIGRATION_VERSION)
async def migrate_up(self):
"""Perform migration up.
Check for case-insensitive duplicate org names and slugs.
If found, rename org/slug as necessary to avoid duplicates
regardless of case.
"""
orgs_db = self.mdb["organizations"]
org_name_set = set()
org_slug_set = set()
cursor = orgs_db.find({})
async for org_dict in cursor:
name = org_dict.get("name", "")
slug = org_dict.get("slug", "")
rename_org = False
if name.lower() in org_name_set:
rename_org = True
else:
org_name_set.add(name.lower())
if slug.lower() in org_slug_set:
rename_org = True
else:
org_slug_set.add(slug.lower())
if rename_org:
await self.update_org_name_and_slug(
orgs_db, org_name_set, org_slug_set, name, org_dict.get("_id")
)
# pylint: disable=too-many-arguments
async def update_org_name_and_slug(
self,
orgs_db,
org_name_set: set[str],
org_slug_set: set[str],
old_name: str,
oid: UUID,
):
"""Rename org"""
count = 2
suffix = f" {count}"
while True:
org_name = f"{old_name}{suffix}"
org_slug = slug_from_name(org_name)
if org_name.lower() in org_name_set or org_slug.lower() in org_slug_set:
count += 1
suffix = f" {count}"
continue
try:
await orgs_db.find_one_and_update(
{"_id": oid}, {"$set": {"slug": org_slug, "name": org_name}}
)
print(
f"Renamed org {oid} to {org_name} with slug {org_slug}", flush=True
)
break
except DuplicateKeyError:
# pylint: disable=raise-missing-from
count += 1
suffix = f" {count}"
# pylint: disable=broad-exception-caught
except Exception as err:
print(f"Error renaming org {oid}: {err}", flush=True)
break