|
| 1 | +""" |
| 2 | +Django management command to update the social auth records UID |
| 3 | +""" |
| 4 | + |
| 5 | +import csv |
| 6 | +import logging |
| 7 | + |
| 8 | +from django.core.exceptions import ValidationError |
| 9 | +from django.core.management.base import BaseCommand |
| 10 | +from django.db import transaction |
| 11 | + |
| 12 | +try: |
| 13 | + from social_django.models import UserSocialAuth |
| 14 | +except ImportError: |
| 15 | + UserSocialAuth = None |
| 16 | + |
| 17 | +logger = logging.getLogger(__name__) |
| 18 | + |
| 19 | + |
| 20 | +class CSVUpdateError(Exception): |
| 21 | + """Custom exception for CSV update process.""" |
| 22 | + pass # pylint: disable=unnecessary-pass |
| 23 | + |
| 24 | + |
| 25 | +class Command(BaseCommand): |
| 26 | + """ |
| 27 | + Update the enterprise related social auth records UID to the new one. |
| 28 | +
|
| 29 | + Example usage: |
| 30 | + ./manage.py lms update_enterprise_social_auth_uids csv_file_path |
| 31 | + ./manage.py lms update_enterprise_social_auth_uids csv_file_path --old-prefix="slug:" --new-prefix="slug:x|{}@xyz" |
| 32 | + ./manage.py lms update_enterprise_social_auth_uids csv_file_path --no-dry-run |
| 33 | +
|
| 34 | + """ |
| 35 | + |
| 36 | + help = 'Records update from CSV with console logging' |
| 37 | + |
| 38 | + def add_arguments(self, parser): |
| 39 | + parser.add_argument('csv_file', type=str, help='Path to the CSV file') |
| 40 | + parser.add_argument( |
| 41 | + '--old_prefix', |
| 42 | + type=str, |
| 43 | + default=None, |
| 44 | + help='Optional old prefix for old UID. If not provided, uses CSV value.' |
| 45 | + ) |
| 46 | + parser.add_argument( |
| 47 | + '--new_prefix', |
| 48 | + type=str, |
| 49 | + default=None, |
| 50 | + help='Optional new prefix for new UID. If not provided, uses CSV value.' |
| 51 | + ) |
| 52 | + parser.add_argument( |
| 53 | + '--no-dry-run', |
| 54 | + action='store_false', |
| 55 | + dest='dry_run', |
| 56 | + default=True, |
| 57 | + help='Actually save changes instead of simulating' |
| 58 | + ) |
| 59 | + |
| 60 | + def handle(self, *args, **options): |
| 61 | + logger.info("Command has started...") |
| 62 | + csv_path = options['csv_file'] |
| 63 | + dry_run = options['dry_run'] |
| 64 | + old_prefix = options['old_prefix'] |
| 65 | + new_prefix = options['new_prefix'] |
| 66 | + |
| 67 | + total_processed = 0 |
| 68 | + total_updated = 0 |
| 69 | + total_errors = 0 |
| 70 | + |
| 71 | + try: |
| 72 | + with open(csv_path, 'r') as csvfile: |
| 73 | + reader = csv.DictReader(csvfile) |
| 74 | + |
| 75 | + for row_num, row in enumerate(reader, start=1): |
| 76 | + total_processed += 1 |
| 77 | + |
| 78 | + try: |
| 79 | + with transaction.atomic(): |
| 80 | + if self.update_record(row, dry_run, old_prefix, new_prefix): |
| 81 | + total_updated += 1 |
| 82 | + |
| 83 | + except Exception as row_error: # pylint: disable=broad-except |
| 84 | + total_errors += 1 |
| 85 | + error_msg = f"Row {row_num} update failed: {row} - Error: {str(row_error)}" |
| 86 | + logger.error(error_msg, exc_info=True) |
| 87 | + |
| 88 | + summary_msg = ( |
| 89 | + f"CSV Update Summary:\n" |
| 90 | + f"Total Records Processed: {total_processed}\n" |
| 91 | + f"Records Successfully Updated: {total_updated}\n" |
| 92 | + f"Errors Encountered: {total_errors}\n" |
| 93 | + f"Dry Run Mode: {'Enabled' if dry_run else 'Disabled'}" |
| 94 | + ) |
| 95 | + logger.info(summary_msg) |
| 96 | + except IOError as io_error: |
| 97 | + logger.critical(f"File I/O error: {str(io_error)}") |
| 98 | + |
| 99 | + except Exception as e: # pylint: disable=broad-except |
| 100 | + logger.critical(f"Critical error in CSV processing: {str(e)}") |
| 101 | + |
| 102 | + def update_record(self, row, dry_run=True, old_prefix=None, new_prefix=None): |
| 103 | + """ |
| 104 | + Update a single record, applying optional prefixes to UIDs if provided. |
| 105 | +
|
| 106 | + Args: |
| 107 | + row (dict): CSV row data |
| 108 | + dry_run (bool): Whether to simulate or actually save changes |
| 109 | + old_prefix (str): Prefix to apply to the old UID |
| 110 | + new_prefix (str): Prefix to apply to the new UID |
| 111 | +
|
| 112 | + Returns: |
| 113 | + bool: Whether the update was successful |
| 114 | + """ |
| 115 | + try: |
| 116 | + old_uid = row.get('old-uid') |
| 117 | + new_uid = row.get('new-uid') |
| 118 | + |
| 119 | + # Validating that both values are present |
| 120 | + if not old_uid or not new_uid: |
| 121 | + raise CSVUpdateError("Missing required UID fields") |
| 122 | + |
| 123 | + # Construct dynamic UIDs |
| 124 | + old_uid_with_prefix = f'{old_prefix}{old_uid}' if old_prefix else old_uid |
| 125 | + new_uid_with_prefix = ( |
| 126 | + new_prefix.format(new_uid) if new_prefix and '{}' in new_prefix |
| 127 | + else f"{new_prefix}{new_uid}" if new_prefix |
| 128 | + else new_uid |
| 129 | + ) |
| 130 | + |
| 131 | + instance_with_old_uid = UserSocialAuth.objects.filter(uid=old_uid_with_prefix).first() |
| 132 | + |
| 133 | + if not instance_with_old_uid: |
| 134 | + raise CSVUpdateError(f"No record found with old UID {old_uid_with_prefix}") |
| 135 | + |
| 136 | + instance_with_new_uid = UserSocialAuth.objects.filter(uid=new_uid_with_prefix).first() |
| 137 | + if instance_with_new_uid: |
| 138 | + log_entry = f"Warning: Existing record with new UID {new_uid_with_prefix} is deleting." |
| 139 | + logger.info(log_entry) |
| 140 | + if not dry_run: |
| 141 | + instance_with_new_uid.delete() |
| 142 | + |
| 143 | + if not dry_run: |
| 144 | + instance_with_old_uid.uid = new_uid_with_prefix |
| 145 | + instance_with_old_uid.save() |
| 146 | + |
| 147 | + log_entry = f"Successfully updated record: Old UID {old_uid_with_prefix} → New UID {new_uid_with_prefix}" |
| 148 | + logger.info(log_entry) |
| 149 | + |
| 150 | + return True |
| 151 | + |
| 152 | + except ValidationError as ve: |
| 153 | + error_msg = f"Validation error: {ve}" |
| 154 | + logger.error(error_msg) |
| 155 | + raise |
| 156 | + |
| 157 | + except CSVUpdateError as update_error: |
| 158 | + error_msg = f"Update processing error: {update_error}" |
| 159 | + logger.error(error_msg) |
| 160 | + raise |
| 161 | + |
| 162 | + except Exception as e: |
| 163 | + error_msg = f"Unexpected error during record update: {e}" |
| 164 | + logger.error(error_msg, exc_info=True) |
| 165 | + raise |
0 commit comments