"""Read-only diagnostics for the legacy mobile uniqueness migration.""" import json from collections import defaultdict from importlib import import_module from django.core.management.base import BaseCommand, CommandError from django.db import connections from apps.cuser.cuser_model import CUser from apps.customer.models import Customer canonical_mobile = import_module( 'apps.customer.migrations.0008_customer_mobile_unique' ).canonical_mobile def _duplicate_groups(model, alias, per_workspace=False): """Use the migration's exact comparison, including country-prefix collisions.""" groups = defaultdict(list) fields = ['pk', 'mobile'] + (['sys_ws_id'] if per_workspace else []) for row in model.objects.using(alias).order_by('pk').values(*fields).iterator(): mobile = canonical_mobile(row['mobile']) if mobile is None: continue key = (row['sys_ws_id'], mobile) if per_workspace else (None, mobile) groups[key].append(row['pk']) return [ {'workspace_id': workspace, 'canonical_mobile': mobile, 'record_ids': ids} for (workspace, mobile), ids in groups.items() if len(ids) > 1 ] def _related_counts(record, alias): """Count direct foreign-key dependents without reading their private contents.""" counts = {} for relation in record._meta.related_objects: if relation.one_to_many or relation.one_to_one: label = f'{relation.related_model._meta.label}.{relation.field.name}' counts[label] = relation.related_model._default_manager.using(alias).filter( **{relation.field.attname: record.pk} ).count() return counts def _customer_details(ids, alias): rows = Customer.objects.using(alias).filter(pk__in=ids).select_related( 'sys_ws', 'cuser_role__cuser', 'cuser_role__role_type' ).order_by('pk') details = [] for row in rows: role = row.cuser_role details.append({ 'id': row.pk, 'name_family': row.name_family, 'mobile': row.mobile, 'canonical_mobile': canonical_mobile(row.mobile), 'workspace_id': row.sys_ws_id, 'workspace_name': row.sys_ws.name, 'panel_role': None if role is None else { 'id': role.pk, 'user_id': role.cuser_id, 'workspace_id': role.sys_ws_id, 'role': role.role_type.role_name_en, 'mobile': role.mobile, 'user_mobile': role.cuser.mobile if role.cuser_id else None, 'is_active': role.is_active, 'customer_panel': role.customer_panel, }, 'related_counts': _related_counts(row, alias), }) return details class Command(BaseCommand): help = 'Report legacy Customer/CUser mobile conflicts; never modify or delete records.' # Must work before migrations, without checking unrelated model declarations. requires_system_checks = [] def add_arguments(self, parser): parser.add_argument('--database', default='default') parser.add_argument('--customer-ids', nargs='+', type=int, default=[]) def handle(self, *args, **options): alias = options['database'] if alias not in connections: raise CommandError(f'Unknown database alias: {alias}') customer_groups = _duplicate_groups(Customer, alias, per_workspace=True) user_groups = _duplicate_groups(CUser, alias) customer_ids = set(options['customer_ids']) customer_ids.update(pk for group in customer_groups for pk in group['record_ids']) user_ids = [pk for group in user_groups for pk in group['record_ids']] report = { 'read_only': True, 'database': alias, 'duplicate_group_count': len(customer_groups) + len(user_groups), 'customer_duplicate_groups': customer_groups, 'user_duplicate_groups': user_groups, 'customers': _customer_details(customer_ids, alias), 'missing_requested_customer_ids': sorted(customer_ids - set( Customer.objects.using(alias).filter(pk__in=customer_ids).values_list('pk', flat=True) )), 'users': [dict(row, canonical_mobile=canonical_mobile(row['mobile'])) for row in CUser.objects.using(alias).filter(pk__in=user_ids).order_by('pk').values( 'id', 'username', 'name_family', 'mobile', 'is_active' )], } self.stdout.write(json.dumps(report, ensure_ascii=False, indent=2))