From 77dee059707c61d7725deda6fa4b9c456e45be86 Mon Sep 17 00:00:00 2001 From: carlos cantillo Date: Sun, 4 Jan 2026 23:18:57 -0500 Subject: [PATCH 1/4] feat: remove validation for retired users on re-register --- common/djangoapps/student/models/user.py | 47 ++++++++---------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 7fe69bd678e4..863f489516dd 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -210,31 +210,13 @@ def user_by_anonymous_id(uid): def is_username_retired(username): """ Checks to see if the given username has been previously retired + + Modified to allow retired usernames to be reused for new registrations. + This enables users to re-register with the same username after account retirement. + The new registration will be a completely separate account with no previous data. """ - locally_hashed_usernames = user_util.get_all_retired_usernames( - username, - settings.RETIRED_USER_SALTS, - settings.RETIRED_USERNAME_FMT - ) - - # TODO: Revert to this after username capitalization issues detailed in - # PLAT-2276, PLAT-2277, PLAT-2278 are sorted out: - # return User.objects.filter(username__in=list(locally_hashed_usernames)).exists() - - # Avoid circular import issues - from openedx.core.djangoapps.user_api.models import UserRetirementStatus - - # Sandbox clean builds attempt to create users during migrations, before the database - # is stable so UserRetirementStatus may not exist yet. This workaround can also go - # when we are done with the username updates. - try: - return User.objects.filter(username__in=list(locally_hashed_usernames)).exists() or \ - UserRetirementStatus.objects.filter(original_username=username).exists() - except ProgrammingError as exc: - # Check the error message to make sure it's what we expect - if "user_api_userretirementstatus" in str(exc): - return User.objects.filter(username__in=list(locally_hashed_usernames)).exists() - raise + # Allow retired usernames to be reused + return False def username_exists_or_retired(username): @@ -247,14 +229,15 @@ def username_exists_or_retired(username): def is_email_retired(email): """ Checks to see if the given email has been previously retired - """ - locally_hashed_emails = user_util.get_all_retired_emails( - email, - settings.RETIRED_USER_SALTS, - settings.RETIRED_EMAIL_FMT - ) - - return User.objects.filter(email__in=list(locally_hashed_emails)).exists() + + Modified to allow retired emails to be reused for new registrations. + This enables users to re-register with the same email after account retirement. + The new registration will be a completely separate account with no previous data. + """ + log.info(f"Checking if email {email} is retired") + print(f"Checking if email {email} is retired") + # Allow retired emails to be reused + return False def email_exists_or_retired(email): From 5719720349b8a27a1add40bb711c04a37a9a46eb Mon Sep 17 00:00:00 2001 From: carlos cantillo Date: Sun, 4 Jan 2026 23:19:42 -0500 Subject: [PATCH 2/4] feat: remove validation for retired users on re-register --- common/djangoapps/student/models/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 863f489516dd..329afa2d20cb 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -234,8 +234,6 @@ def is_email_retired(email): This enables users to re-register with the same email after account retirement. The new registration will be a completely separate account with no previous data. """ - log.info(f"Checking if email {email} is retired") - print(f"Checking if email {email} is retired") # Allow retired emails to be reused return False From 4db2f2d745e419d8b6a0ca86c9f295c5cad81939 Mon Sep 17 00:00:00 2001 From: carlos cantillo Date: Mon, 5 Jan 2026 15:36:13 -0500 Subject: [PATCH 3/4] fix: restored user deletion flow --- openedx/core/djangoapps/user_api/accounts/utils.py | 11 +++++++++-- openedx/core/djangoapps/user_api/models.py | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py index 826dbd42cd13..f325b931e2ee 100644 --- a/openedx/core/djangoapps/user_api/accounts/utils.py +++ b/openedx/core/djangoapps/user_api/accounts/utils.py @@ -200,6 +200,8 @@ def create_retirement_request_and_deactivate_account(user): """ Adds user to retirement queue, unlinks social auth accounts, changes user passwords and delete tokens and activation keys + + Modified to include user ID in retired credentials to allow reuse of original credentials. """ # Add user to retirement queue. UserRetirementStatus.create_retirement(user) @@ -207,8 +209,13 @@ def create_retirement_request_and_deactivate_account(user): # Unlink LMS social auth accounts UserSocialAuth.objects.filter(user_id=user.id).delete() - # Change LMS password & email - user.email = get_retired_email_by_email(user.email) + # Change LMS password, username & email + # Include user ID to ensure uniqueness when retired credentials are reused + retired_username = f"retired__user_{user.id}_{user.username}" + retired_email = f"retired__user_{user.id}_{user.email.split('@')[0]}@retired.invalid" + + user.username = retired_username + user.email = retired_email user.set_unusable_password() user.save() diff --git a/openedx/core/djangoapps/user_api/models.py b/openedx/core/djangoapps/user_api/models.py index d776dac8fe2a..1c0e46c072a9 100644 --- a/openedx/core/djangoapps/user_api/models.py +++ b/openedx/core/djangoapps/user_api/models.py @@ -340,6 +340,8 @@ def create_retirement(cls, user): """ Creates a UserRetirementStatus for the given user, in the correct initial state. Will fail if the user already has a UserRetirementStatus row or if states are not yet populated. + + Modified to include user ID in retired credentials to allow reuse of original credentials. """ try: pending = RetirementState.objects.all().order_by('state_execution_order')[0] @@ -349,8 +351,9 @@ def create_retirement(cls, user): if cls.objects.filter(user=user).exists(): raise RetirementStateError(f'User {user} already has a retirement status row!') - retired_username = get_retired_username_by_username(user.username) - retired_email = get_retired_email_by_email(user.email) + # Include user ID to ensure uniqueness when retired credentials are reused + retired_username = f"retired__user_{user.id}_{user.username}" + retired_email = f"retired__user_{user.id}_{user.email.split('@')[0]}@retired.invalid" UserRetirementRequest.create_retirement_request(user) From 50bb450c2ff69a026871c87604ac532823392f65 Mon Sep 17 00:00:00 2001 From: carlos cantillo Date: Mon, 5 Jan 2026 15:51:09 -0500 Subject: [PATCH 4/4] fix: lint white space issues --- common/djangoapps/student/models/user.py | 4 ++-- openedx/core/djangoapps/user_api/accounts/utils.py | 4 ++-- openedx/core/djangoapps/user_api/models.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 329afa2d20cb..283a1efb2099 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -210,7 +210,7 @@ def user_by_anonymous_id(uid): def is_username_retired(username): """ Checks to see if the given username has been previously retired - + Modified to allow retired usernames to be reused for new registrations. This enables users to re-register with the same username after account retirement. The new registration will be a completely separate account with no previous data. @@ -229,7 +229,7 @@ def username_exists_or_retired(username): def is_email_retired(email): """ Checks to see if the given email has been previously retired - + Modified to allow retired emails to be reused for new registrations. This enables users to re-register with the same email after account retirement. The new registration will be a completely separate account with no previous data. diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py index f325b931e2ee..36b82f8d8546 100644 --- a/openedx/core/djangoapps/user_api/accounts/utils.py +++ b/openedx/core/djangoapps/user_api/accounts/utils.py @@ -200,7 +200,7 @@ def create_retirement_request_and_deactivate_account(user): """ Adds user to retirement queue, unlinks social auth accounts, changes user passwords and delete tokens and activation keys - + Modified to include user ID in retired credentials to allow reuse of original credentials. """ # Add user to retirement queue. @@ -213,7 +213,7 @@ def create_retirement_request_and_deactivate_account(user): # Include user ID to ensure uniqueness when retired credentials are reused retired_username = f"retired__user_{user.id}_{user.username}" retired_email = f"retired__user_{user.id}_{user.email.split('@')[0]}@retired.invalid" - + user.username = retired_username user.email = retired_email user.set_unusable_password() diff --git a/openedx/core/djangoapps/user_api/models.py b/openedx/core/djangoapps/user_api/models.py index 1c0e46c072a9..8d54d01c3695 100644 --- a/openedx/core/djangoapps/user_api/models.py +++ b/openedx/core/djangoapps/user_api/models.py @@ -340,7 +340,7 @@ def create_retirement(cls, user): """ Creates a UserRetirementStatus for the given user, in the correct initial state. Will fail if the user already has a UserRetirementStatus row or if states are not yet populated. - + Modified to include user ID in retired credentials to allow reuse of original credentials. """ try: