Files
crmApi/apps/customer/mobile_migration_repair.py
T
2026-10-08 18:09:12 +03:30

89 lines
4.4 KiB
Python

"""Frozen, historical-model-only repair used by migration 0008."""
from collections import defaultdict
from importlib import import_module
import logging
from django.db.migrations.state import ProjectState
def _groups(model, alias, canonical, workspace=False):
groups = defaultdict(list)
for row in model.objects.using(alias).order_by('pk').iterator():
mobile = canonical(row.mobile)
if mobile is not None:
groups[(row.sys_ws_id if workspace else None, mobile)].append(row)
return {key: rows for key, rows in groups.items() if len(rows) > 1}
def _audit_model(schema_editor):
# Render the frozen 0009 schema; never depend on future live model fields.
operation = import_module('apps.customer.migrations.0009_mobileconflict').Migration.operations[0]
state = ProjectState()
operation.state_forwards('customer', state)
model = state.apps.get_model('customer', 'MobileConflict')
if model._meta.db_table not in schema_editor.connection.introspection.table_names():
schema_editor.create_model(model)
return model
def _archive(audit, alias, row, mobile, snapshot, workspace=None):
audit.objects.using(alias).get_or_create(
source_model=row._meta.label, record_id=row.pk,
defaults={'workspace_id': workspace, 'original_mobile': row.mobile,
'canonical_mobile': mobile, 'snapshot': snapshot},
)
def repair_conflicts(apps, schema_editor, canonical):
"""Quarantine ambiguous identities; never choose an owner or merge/delete rows."""
alias = schema_editor.connection.alias
customer = apps.get_model('customer', 'Customer')
user = apps.get_model('cuser', 'CUser')
role = apps.get_model('cuser', 'CuserRole')
if schema_editor.connection.vendor == 'postgresql':
# This migration is atomic. Prevent legacy writers inserting new conflicts
# between the audit and the unique constraint, including empty tables.
tables = ', '.join(schema_editor.connection.ops.quote_name(model._meta.db_table)
for model in (customer, user, role))
with schema_editor.connection.cursor() as cursor:
cursor.execute(f'LOCK TABLE {tables} IN SHARE ROW EXCLUSIVE MODE')
customer_groups = _groups(customer, alias, canonical, workspace=True)
user_groups = _groups(user, alias, canonical)
if not customer_groups and not user_groups:
return set()
audit = _audit_model(schema_editor)
for (workspace, mobile), rows in customer_groups.items():
ids = [row.pk for row in rows]
for row in rows:
roles = list(role.objects.using(alias).filter(pk=row.cuser_role_id).values(
'id', 'cuser_id', 'sys_ws_id', 'mobile', 'is_active', 'customer_panel'
))
_archive(audit, alias, row, mobile, {
'reason': 'duplicate_customer_mobile', 'conflicting_record_ids': ids,
'name_family': row.name_family, 'cuser_role_id': row.cuser_role_id,
'roles': roles, 'action': 'clear_mobile_and_detach_panel',
}, workspace)
role_ids = [row.cuser_role_id for row in rows if row.cuser_role_id is not None]
role.objects.using(alias).filter(pk__in=role_ids).update(customer_panel=False)
customer.objects.using(alias).filter(pk__in=ids).update(mobile=None, cuser_role_id=None)
skipped_users = set()
for (_, mobile), rows in user_groups.items():
ids = [row.pk for row in rows]
for row in rows:
_archive(audit, alias, row, mobile, {
'reason': 'duplicate_account_mobile', 'conflicting_record_ids': ids,
'username': row.username, 'name_family': row.name_family, 'is_active': row.is_active,
'roles': list(role.objects.using(alias).filter(cuser_id=row.pk).values(
'id', 'sys_ws_id', 'mobile', 'is_active', 'customer_panel'
)), 'action': 'disable_ambiguous_accounts_preserve_mobile',
})
user.objects.using(alias).filter(pk__in=ids).update(is_active=False)
role.objects.using(alias).filter(cuser_id__in=ids).update(is_active=False, customer_panel=False)
skipped_users.update(ids)
logging.getLogger(__name__).warning(
'Legacy mobile conflicts quarantined: %s customers, %s accounts. '
'No records deleted. Review customer.MobileConflict in Django Admin.',
sum(len(rows) for rows in customer_groups.values()), len(skipped_users),
)
return skipped_users