89 lines
4.4 KiB
Python
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
|