Skip to content

Commit aa2b24e

Browse files
authored
feat: Added management command to update UIDs (#2300)
1 parent 1ed53c2 commit aa2b24e

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Unreleased
1717
----------
1818
* nothing unreleased
1919

20+
[5.4.2]
21+
--------
22+
* feat: Added a management command to update the Social Auth UID's for an enterprise.
23+
2024
[5.4.1]
2125
--------
2226
* fix: The default enrollment ``learner_status`` view now considers intended courses

enterprise/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Your project description goes here.
33
"""
44

5-
__version__ = "5.4.1"
5+
__version__ = "5.4.2"
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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

Comments
 (0)