From 68e2f291a66378c7749545a1b88f9dd2b6adb4c2 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 17:29:11 -0500 Subject: [PATCH 01/11] feat: remove `REGISTRATION_EXTENSION_FORM` setting --- docs/concepts/extension_points.rst | 7 --- lms/envs/common.py | 15 ------ .../core/djangoapps/user_authn/api/helper.py | 1 - .../user_authn/views/registration_form.py | 49 ++----------------- .../user_authn/views/tests/test_register.py | 14 +++--- 5 files changed, 12 insertions(+), 74 deletions(-) diff --git a/docs/concepts/extension_points.rst b/docs/concepts/extension_points.rst index cfc2c38fd689..37276e55b265 100644 --- a/docs/concepts/extension_points.rst +++ b/docs/concepts/extension_points.rst @@ -122,13 +122,6 @@ Here are the different integration points that python plugins can use: * - Custom profile extension form app (``PROFILE_EXTENSION_FORM`` Django setting in the LMS) - Trial, Stable - By default, the registration page for each instance of Open edX has fields that ask for information such as a user’s name, country, and highest level of education completed. You can add custom fields to the registration page and user profile for your own Open edX instance. These fields can be different types, including text entry fields and drop-down lists. See `Adding Custom Fields to the Registration Page`_. - - **Important Migration Note:** - - - ``REGISTRATION_EXTENSION_FORM`` (deprecated) continues to work with old behavior: custom fields only for registration, data stored in UserProfile.meta - - ``PROFILE_EXTENSION_FORM`` (new) enables new capabilities: custom fields in registration and account settings, data stored in dedicated model - - Sites using the deprecated setting will maintain backward compatibility. To get the new capabilities, migrate to ``PROFILE_EXTENSION_FORM``. * - Learning Context (``openedx.learning_context``) - Trial, Limited - A "Learning Context" is a course, a library, a program, a blog, an external site, or some other collection of content where learning happens. If you are trying to build a totally new learning experience that's not a type of course, you may need to implement a new learning context. Learning contexts are a new abstraction and are only supported in the nascent openedx_content-based XBlock runtime. Since existing courses use modulestore instead of openedx_content, they are not yet implemented as learning contexts. However, openedx_content-based content libraries are. See |learning_context.py|_ to learn more. diff --git a/lms/envs/common.py b/lms/envs/common.py index 3b7133836d9c..17ccbc1fb456 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2628,32 +2628,17 @@ # Note: If you want to use a model to store the results of the form, you will # need to add the model's app to the ADDL_INSTALLED_APPS array in your # lms.yml file. -# -# REGISTRATION_EXTENSION_FORM is deprecated but will continue to work for backward compatibility. -# Sites using this setting will maintain the old behavior: -# - Data is stored in UserProfile.meta JSON field -# - No ability to update extended fields after registration via account settings API -# -# To get new capabilities (model-based storage), migrate to PROFILE_EXTENSION_FORM. -REGISTRATION_EXTENSION_FORM = None # DEPRECATED: Use PROFILE_EXTENSION_FORM instead # PROFILE_EXTENSION_FORM is a Django ModelForm class used for extending user profiles # beyond the default fields. This setting enables new capabilities for profile management: # - Data is stored in a dedicated model (not just UserProfile.meta) # - Users can update their extended profile fields via the account settings API # -# This setting supersedes REGISTRATION_EXTENSION_FORM and provides more accurate naming -# for profile extension functionality. -# # Example: PROFILE_EXTENSION_FORM = 'myapp.forms.ExtendedProfileForm' # # The custom form's model should have: # - A OneToOneField to User (typically named 'user') # - Additional fields for extended profile data -# -# MIGRATION NOTE: If you're currently using REGISTRATION_EXTENSION_FORM (deprecated), -# your custom fields will continue working as before (data in meta field). -# To get the new capabilities, migrate to PROFILE_EXTENSION_FORM. PROFILE_EXTENSION_FORM = None # Identifier included in the User Agent from Open edX mobile apps. diff --git a/openedx/core/djangoapps/user_authn/api/helper.py b/openedx/core/djangoapps/user_authn/api/helper.py index 9e4c8104b0ff..2af77244d728 100644 --- a/openedx/core/djangoapps/user_authn/api/helper.py +++ b/openedx/core/djangoapps/user_authn/api/helper.py @@ -101,7 +101,6 @@ def get_fields(self): Returns the required or optional fields configured in REGISTRATION_EXTRA_FIELDS settings. """ # Custom form fields can be added via the form set in settings.PROFILE_EXTENSION_FORM - # (or deprecated settings.REGISTRATION_EXTENSION_FORM) custom_form = get_registration_extension_form() or {} response = {} for field in self.valid_fields: diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 77984aa2bce7..c51a1e7beb64 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -319,34 +319,15 @@ def clean_country(self): def get_registration_extension_form(*args, **kwargs) -> forms.Form | None: """ - Convenience function for getting the custom form set in settings.PROFILE_EXTENSION_FORM - or settings.REGISTRATION_EXTENSION_FORM (deprecated). + Convenience function for getting the custom form set in settings.PROFILE_EXTENSION_FORM. - Returns an instance of the configured profile extension form. - - The function first checks for PROFILE_EXTENSION_FORM (recommended), then falls back to - REGISTRATION_EXTENSION_FORM for backwards compatibility. When REGISTRATION_EXTENSION_FORM - is used, a deprecation warning is logged. - - An example form app for this can be found at http://github.com/open-craft/custom-form-app + Returns an instance of the configured profile extension form. An example form app + for this can be found at http://github.com/open-craft/custom-form-app Returns: Form instance or None if no form is configured """ - # Check for the new setting first setting_value = getattr(settings, "PROFILE_EXTENSION_FORM", None) - setting_name = "PROFILE_EXTENSION_FORM" - - # Fall back to the deprecated setting - if not setting_value: - setting_value = getattr(settings, "REGISTRATION_EXTENSION_FORM", None) - if setting_value: - setting_name = "REGISTRATION_EXTENSION_FORM" - log.warning( - "REGISTRATION_EXTENSION_FORM is deprecated and will be removed in a future release. " - "Please use PROFILE_EXTENSION_FORM instead. Current value: %s", - setting_value, - ) if not setting_value: return None @@ -356,7 +337,7 @@ def get_registration_extension_form(*args, **kwargs) -> forms.Form | None: module = import_module(module) return getattr(module, klass)(*args, **kwargs) except (ValueError, ImportError, AttributeError) as e: - log.error("Could not load form from %s='%s': %s", setting_name, setting_value, str(e)) + log.error("Could not load form from PROFILE_EXTENSION_FORM='%s': %s", setting_value, str(e)) return None @@ -367,33 +348,14 @@ def get_extended_profile_model() -> type[Model] | None: Returns the Django model class associated with the form specified in the `PROFILE_EXTENSION_FORM` setting. - IMPORTANT: This function only works with PROFILE_EXTENSION_FORM. If you're using - the deprecated REGISTRATION_EXTENSION_FORM, this will return None to maintain - backward compatibility. The new profile extension capabilities (loading/saving - to a custom model) are only available when using PROFILE_EXTENSION_FORM. - - Migration path: - - Old behavior (REGISTRATION_EXTENSION_FORM): Custom fields only for registration, - data stored in UserProfile.meta field - - New behavior (PROFILE_EXTENSION_FORM): Custom fields for registration and profile, - data stored in dedicated model with ability to load/update via account settings API - Returns: type[Model] | None: The model class if PROFILE_EXTENSION_FORM is configured - and valid, None otherwise (including when using the deprecated - REGISTRATION_EXTENSION_FORM). + and valid, None otherwise. Examples: - # New setting with model support: # In settings.py: PROFILE_EXTENSION_FORM = 'myapp.forms.ExtendedProfileForm' model_class = get_extended_profile_model() # Returns the model - - # Deprecated setting - maintains old behavior: - # In settings.py: REGISTRATION_EXTENSION_FORM = 'myapp.forms.ExtendedForm' - model_class = get_extended_profile_model() # Returns None (no model support) """ - # Only check for the new setting - do NOT fall back to REGISTRATION_EXTENSION_FORM - # This ensures backward compatibility: users of the old setting keep the old behavior setting_value = getattr(settings, "PROFILE_EXTENSION_FORM", None) if not setting_value: @@ -584,7 +546,6 @@ def get_registration_form(self, request): self._apply_third_party_auth_overrides(request, form_desc) # Custom form fields can be added via the form set in settings.PROFILE_EXTENSION_FORM - # (or deprecated settings.REGISTRATION_EXTENSION_FORM) custom_form = get_registration_extension_form() if custom_form: custom_form_field_names = [field_name for field_name, field in custom_form.fields.items()] diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 5ba40a6c960e..1680f6d8b894 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -672,7 +672,7 @@ def test_register_form_password_complexity(self): } ) - @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm') + @override_settings(PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm') def test_extension_form_fields(self): no_extra_fields_setting = {} @@ -1310,7 +1310,7 @@ def test_registration_separate_terms_of_service_mktg_site_disabled(self): "confirm_email": "required", }, REGISTRATION_FIELD_ORDER=None, - REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', + PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', ) def test_field_order(self): response = self.client.get(self.url) @@ -1397,7 +1397,7 @@ def test_field_order_override(self): "honor_code": "required", "confirm_email": "required", }, - REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', + PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', REGISTRATION_FIELD_ORDER=[ "name", "confirm_email", @@ -1504,7 +1504,7 @@ def test_register_with_profile_info(self): assert account_settings["goals"] == self.GOALS assert account_settings["country"] == self.COUNTRY - @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm') + @override_settings(PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm') @mock.patch('openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm.DUMMY_STORAGE', new_callable=dict) @mock.patch( 'openedx.core.djangoapps.user_api.tests.test_helpers.DummyRegistrationExtensionModel', @@ -2072,7 +2072,7 @@ def setUp(self): # pylint: disable=arguments-differ "honor_code": "required", "confirm_email": "required", }, - REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', + PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', REGISTRATION_FIELD_ORDER=[ "name", "confirm_email", @@ -2180,7 +2180,7 @@ def test_field_order_override(self): "confirm_email": "required", }, REGISTRATION_FIELD_ORDER=None, - REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', + PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', ) def test_field_order(self): response = self.client.get(self.url) @@ -2222,7 +2222,7 @@ def test_field_order(self): "confirm_email": "required", }, REGISTRATION_FIELD_ORDER=None, - REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', + PROFILE_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm', ) def test_year_of_birth_field_with_feature_flag(self): """ From 169f515d5907f28acb57e40106ebe8750ffe9c0c Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 17:53:44 -0500 Subject: [PATCH 02/11] refactor: update documentation for PROFILE_EXTENSION_FORM setting --- lms/envs/common.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 17ccbc1fb456..3a6c7349f2ee 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2620,25 +2620,21 @@ FINANCIAL_ASSISTANCE_MIN_LENGTH = 1250 FINANCIAL_ASSISTANCE_MAX_LENGTH = 2500 -#### Registration form extension. #### -# Only used if combined login/registration is enabled. -# This can be used to add fields to the registration page. -# It must be a path to a valid form, in dot-separated syntax. -# IE: custom_form_app.forms.RegistrationExtensionForm -# Note: If you want to use a model to store the results of the form, you will -# need to add the model's app to the ADDL_INSTALLED_APPS array in your -# lms.yml file. - -# PROFILE_EXTENSION_FORM is a Django ModelForm class used for extending user profiles -# beyond the default fields. This setting enables new capabilities for profile management: -# - Data is stored in a dedicated model (not just UserProfile.meta) -# - Users can update their extended profile fields via the account settings API -# -# Example: PROFILE_EXTENSION_FORM = 'myapp.forms.ExtendedProfileForm' +##### User Profile Extension ##### + +# PROFILE_EXTENSION_FORM is a Django ModelForm used to add extended user +# fields to the platform. This setting integrates these fields into both +# the registration form and the account settings api. + +# Usage: +# - Set this to the dot-separated path of a valid ModelForm class. +# Example: PROFILE_EXTENSION_FORM = 'my_custom_app.forms.ExtendedProfileForm' # -# The custom form's model should have: -# - A OneToOneField to User (typically named 'user') -# - Additional fields for extended profile data +# Requirements: +# 1. Linked Model: The form must be based on a model with a OneToOneField to +# the User (typically named 'user'). +# 2. Installed Apps: Ensure the app containing your model is included in +# the ADDL_INSTALLED_APPS array. PROFILE_EXTENSION_FORM = None # Identifier included in the User Agent from Open edX mobile apps. From ad7e1a507a8ac30e2cab7cedf8f8d2f86f31b0c7 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 18:12:48 -0500 Subject: [PATCH 03/11] refactor: rename extended profile functions --- .../djangoapps/user_api/accounts/forms.py | 8 ++-- .../user_api/accounts/serializers.py | 4 +- .../core/djangoapps/user_authn/api/helper.py | 6 +-- .../djangoapps/user_authn/views/register.py | 4 +- .../user_authn/views/registration_form.py | 38 +++++++++++-------- .../user_authn/views/tests/test_utils.py | 34 ++++++++--------- 6 files changed, 51 insertions(+), 43 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/forms.py b/openedx/core/djangoapps/user_api/accounts/forms.py index 336eb85f9cf9..6e02ce30e09b 100644 --- a/openedx/core/djangoapps/user_api/accounts/forms.py +++ b/openedx/core/djangoapps/user_api/accounts/forms.py @@ -11,8 +11,8 @@ from common.djangoapps.student.models import User from openedx.core.djangoapps.user_api.accounts.utils import handle_retirement_cancellation from openedx.core.djangoapps.user_authn.views.registration_form import ( - get_extended_profile_model, - get_registration_extension_form, + get_profile_extension_model, + get_profile_extension_form, ) logger = logging.getLogger(__name__) @@ -108,7 +108,7 @@ def get_extended_profile_form( - field_errors (dict): Dictionary of validation errors, if any """ field_errors, kwargs = {}, {} - extended_profile_model = get_extended_profile_model() + extended_profile_model = get_profile_extension_model() try: kwargs["instance"] = extended_profile_model.objects.get(user=user) @@ -118,7 +118,7 @@ def get_extended_profile_form( logger.info("No existing extended profile found for user %s, creating new instance", user.username) try: - extended_profile_form = get_registration_extension_form(data=extended_profile_fields_data, **kwargs) + extended_profile_form = get_profile_extension_form(data=extended_profile_fields_data, **kwargs) except Exception as e: # pylint: disable=broad-exception-caught logger.error("Unexpected error creating custom form for user %s: %s", user.username, str(e)) field_errors["extended_profile"] = { diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 2d6a8b384881..f4b25d1317d0 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -29,7 +29,7 @@ from openedx.core.djangoapps.user_authn.views.registration_form import ( contains_html, contains_url, - get_extended_profile_model, + get_profile_extension_model, ) from openedx.features.name_affirmation_api.utils import get_name_affirmation_service @@ -600,7 +600,7 @@ def get_extended_profile(user_profile: UserProfile) -> list[dict[str, str]]: """ def get_extended_profile_data(): - extended_profile_model = get_extended_profile_model() + extended_profile_model = get_profile_extension_model() if extended_profile_model: try: diff --git a/openedx/core/djangoapps/user_authn/api/helper.py b/openedx/core/djangoapps/user_authn/api/helper.py index 2af77244d728..75ebeb10a842 100644 --- a/openedx/core/djangoapps/user_authn/api/helper.py +++ b/openedx/core/djangoapps/user_authn/api/helper.py @@ -9,7 +9,7 @@ from common.djangoapps.student.models import UserProfile from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_authn.api import form_fields -from openedx.core.djangoapps.user_authn.views.registration_form import get_registration_extension_form +from openedx.core.djangoapps.user_authn.views.registration_form import get_profile_extension_form class RegistrationFieldsContext(APIView): @@ -74,7 +74,7 @@ def __init__(self, field_type='required'): field for field in ordered_extra_fields if self._fields_setting.get(field) == self.field_type ] - custom_form = get_registration_extension_form() + custom_form = get_profile_extension_form() if custom_form: for field_name, field in custom_form.fields.items(): # If the field_type is required make sure the custom field is required in the form and if the @@ -101,7 +101,7 @@ def get_fields(self): Returns the required or optional fields configured in REGISTRATION_EXTRA_FIELDS settings. """ # Custom form fields can be added via the form set in settings.PROFILE_EXTENSION_FORM - custom_form = get_registration_extension_form() or {} + custom_form = get_profile_extension_form() or {} response = {} for field in self.valid_fields: if field == 'confirm_email' and self.field_type == 'optional' or not self._field_can_be_saved(field): diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index 4a78ee83ef06..d36b43b646d0 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -86,7 +86,7 @@ from openedx.core.djangoapps.user_authn.views.registration_form import ( AccountCreationForm, RegistrationFormFactory, - get_registration_extension_form, + get_profile_extension_form, ) from openedx.core.djangoapps.user_authn.views.utils import get_auto_generated_username @@ -205,7 +205,7 @@ def create_account_with_params(request, params): # pylint: disable=too-many-sta do_third_party_auth=False, tos_required=tos_required, ) - custom_form = get_registration_extension_form(data=params) + custom_form = get_profile_extension_form(data=params) is_marketable = params.get('marketing_emails_opt_in') in ['true', '1'] # Perform operations within a transaction that are critical to account creation diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index c51a1e7beb64..4e035acd391d 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -317,15 +317,23 @@ def clean_country(self): return self.cleaned_data.get("country") -def get_registration_extension_form(*args, **kwargs) -> forms.Form | None: +def get_profile_extension_form(*args, **kwargs) -> forms.Form | None: """ - Convenience function for getting the custom form set in settings.PROFILE_EXTENSION_FORM. + Get an instance of the custom profile extension form. - Returns an instance of the configured profile extension form. An example form app - for this can be found at http://github.com/open-craft/custom-form-app + This form is configured via the `PROFILE_EXTENSION_FORM` Django setting + + Args: + *args: Variable length argument list passed to the form's __init__ method. + **kwargs: Arbitrary keyword arguments passed to the form's __init__ method. Returns: - Form instance or None if no form is configured + forms.Form | None: An initialized form instance, or None if the setting + is not configured or the form fails to load. + + References: + An example form app for this can be found at: + http://github.com/open-craft/custom-form-app """ setting_value = getattr(settings, "PROFILE_EXTENSION_FORM", None) @@ -341,20 +349,20 @@ def get_registration_extension_form(*args, **kwargs) -> forms.Form | None: return None -def get_extended_profile_model() -> type[Model] | None: +def get_profile_extension_model() -> type[Model] | None: """ - Get the model class for the extended profile form. + Get the model class associated with the custom profile extension form. - Returns the Django model class associated with the form specified in - the `PROFILE_EXTENSION_FORM` setting. + This extracts the model from the Meta class of the form specified in + the `PROFILE_EXTENSION_FORM` Django setting. Returns: - type[Model] | None: The model class if PROFILE_EXTENSION_FORM is configured - and valid, None otherwise. + type[Model] | None: The Django model class if the setting is configured + and valid, None otherwise. Examples: - # In settings.py: PROFILE_EXTENSION_FORM = 'myapp.forms.ExtendedProfileForm' - model_class = get_extended_profile_model() # Returns the model + # In settings.py: PROFILE_EXTENSION_FORM = 'myapp.forms.ExtensionForm' + model_class = get_profile_extension_model() """ setting_value = getattr(settings, "PROFILE_EXTENSION_FORM", None) @@ -453,7 +461,7 @@ def __init__(self): handler = getattr(self, f"_add_{field_name}_field") self.field_handlers[field_name] = handler - custom_form = get_registration_extension_form() + custom_form = get_profile_extension_form() if custom_form: custom_form_field_names = [field_name for field_name, field in custom_form.fields.items()] valid_fields.extend(custom_form_field_names) @@ -546,7 +554,7 @@ def get_registration_form(self, request): self._apply_third_party_auth_overrides(request, form_desc) # Custom form fields can be added via the form set in settings.PROFILE_EXTENSION_FORM - custom_form = get_registration_extension_form() + custom_form = get_profile_extension_form() if custom_form: custom_form_field_names = [field_name for field_name, field in custom_form.fields.items()] else: diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py index e9fb7f71a81c..cf0feee323c9 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py @@ -11,8 +11,8 @@ from django.test.utils import override_settings from openedx.core.djangoapps.user_authn.views.registration_form import ( - get_extended_profile_model, - get_registration_extension_form, + get_profile_extension_model, + get_profile_extension_form, ) from openedx.core.djangoapps.user_authn.views.utils import _get_username_prefix, get_auto_generated_username @@ -100,7 +100,7 @@ def test_get_extended_profile_model_no_setting_or_empty_string(self, setting_val Test when `PROFILE_EXTENSION_FORM` setting is not configured """ with override_settings(PROFILE_EXTENSION_FORM=setting_value): - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 @@ -110,7 +110,7 @@ def test_get_extended_profile_model_invalid_module(self, mock_logger: Mock): """ Test when the module path is invalid """ - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 mock_logger.warning.assert_called_once() @@ -121,7 +121,7 @@ def test_get_extended_profile_model_no_meta_class(self): """ Test when the form class doesn't have a Meta class """ - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 @@ -131,7 +131,7 @@ def test_get_extended_profile_model_malformed_path(self, mock_logger: Mock): """ Test when the setting value doesn't have a dot separator """ - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 mock_logger.warning.assert_called_once() @@ -151,7 +151,7 @@ def test_get_extended_profile_model_custom_form(self, mock_import_module: Mock): mock_module.CustomExtendedProfileForm = mock_form_class mock_import_module.return_value = mock_module - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertEqual(result, mock_model) # noqa: PT009 mock_import_module.assert_called_once_with("myapp.forms") @@ -170,7 +170,7 @@ def test_get_extended_profile_model_form_without_model(self, mock_import_module: mock_module.FormWithoutModel = mock_form_class mock_import_module.return_value = mock_module - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 @@ -187,7 +187,7 @@ def test_get_extended_profile_model_import_errors( """ mock_import_module.side_effect = exception_class(error_message) - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 mock_logger.warning.assert_called_once() @@ -203,7 +203,7 @@ def test_get_extended_profile_model_attribute_error(self, mock_logger: Mock, moc mock_module = Mock(spec=[]) mock_import_module.return_value = mock_module - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 mock_logger.warning.assert_called_once() @@ -218,7 +218,7 @@ def test_get_extended_profile_model_with_deprecated_setting_returns_none(self): will NOT get the new model-based profile capabilities. They continue using the old UserProfile.meta field approach. """ - result = get_extended_profile_model() + result = get_profile_extension_model() self.assertIsNone(result) # noqa: PT009 @@ -235,7 +235,7 @@ def test_get_registration_extension_form_no_setting(self, setting_value: str | N Test when neither PROFILE_EXTENSION_FORM nor REGISTRATION_EXTENSION_FORM is configured """ with override_settings(PROFILE_EXTENSION_FORM=setting_value, REGISTRATION_EXTENSION_FORM=setting_value): - result = get_registration_extension_form() + result = get_profile_extension_form() self.assertIsNone(result) # noqa: PT009 @@ -251,7 +251,7 @@ def test_get_registration_extension_form_with_new_setting(self, mock_import_modu mock_module.CustomProfileForm = mock_form_class mock_import_module.return_value = mock_module - result = get_registration_extension_form(data={"field": "value"}) + result = get_profile_extension_form(data={"field": "value"}) self.assertEqual(result, mock_form_instance) # noqa: PT009 mock_import_module.assert_called_once_with("myapp.forms") @@ -269,7 +269,7 @@ def test_get_registration_extension_form_new_setting_precedence(self, mock_impor mock_module.NewForm = mock_form_class mock_import_module.return_value = mock_module - result = get_registration_extension_form() + result = get_profile_extension_form() self.assertEqual(result, mock_form_instance) # noqa: PT009 mock_import_module.assert_called_once_with("myapp.forms") @@ -287,7 +287,7 @@ def test_get_registration_extension_form_deprecation_warning(self, mock_logger: mock_module.LegacyForm = mock_form_class mock_import_module.return_value = mock_module - result = get_registration_extension_form() + result = get_profile_extension_form() self.assertEqual(result, mock_form_instance) # noqa: PT009 deprecation_calls = [call for call in mock_logger.warning.call_args_list if "deprecated" in str(call).lower()] @@ -305,7 +305,7 @@ def test_get_registration_extension_form_import_error(self, mock_logger: Mock, m """ mock_import_module.side_effect = ImportError("Module not found") - result = get_registration_extension_form() + result = get_profile_extension_form() self.assertIsNone(result) # noqa: PT009 error_calls = mock_logger.error.call_args_list @@ -317,7 +317,7 @@ def test_get_registration_extension_form_malformed_path(self, mock_logger: Mock) """ Test when setting value doesn't have proper format (no dot separator) """ - result = get_registration_extension_form() + result = get_profile_extension_form() self.assertIsNone(result) # noqa: PT009 From 945a1dce244410d418072895bece8232e0e28217 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 18:49:17 -0500 Subject: [PATCH 04/11] refactor: improve handling of extended profile data retrieval --- .../user_api/accounts/serializers.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index f4b25d1317d0..cc148eb3de46 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -577,18 +577,19 @@ def get_extended_profile(user_profile: UserProfile) -> list[dict[str, str]]: This function extracts custom profile fields that extend beyond the standard UserProfile model. It prefers data from a custom extended profile model - (when configured), and only uses the `user_profile.meta` JSON field when - no such model is configured. The returned data is filtered to include only - fields specified in the `extended_profile_fields` site configuration. + (when configured), falling back to the `user_profile.meta` JSON field for + any field not present in the model (or when the user has no model record yet). - The function supports two data sources: + The returned data is filtered to include only fields specified in the + `extended_profile_fields` site configuration. + + The function supports two data sources (applied per field): 1. Custom model: If the `PROFILE_EXTENSION_FORM` setting points to a form with a - `Meta.model`, data is retrieved from that model using `model_to_dict()`. If a - model is configured but the user does not yet have a corresponding record, - this function returns an empty mapping for extended profile fields (it does - not fall back to `user_profile.meta` in that case). - 2. Fallback: JSON data stored in `UserProfile.meta` field, used only when no - custom extended profile model is configured. + `Meta.model`, data is retrieved from that model using `model_to_dict()`. + Fields not present in the model, or fields when the user has no model record, + fall back to `user_profile.meta`. + 2. Fallback: JSON data stored in `UserProfile.meta` field, used when no custom + extended profile model is configured or when the model lacks a given field. Args: user_profile (UserProfile): The user profile instance to get extended fields from. @@ -596,23 +597,26 @@ def get_extended_profile(user_profile: UserProfile) -> list[dict[str, str]]: Returns: list[dict[str, str]]: A list of dictionaries, each containing: - field_name: The name of the extended profile field - - field_value: The value of the field (converted to string) + - field_value: The value of the field """ def get_extended_profile_data(): - extended_profile_model = get_profile_extension_model() + try: + meta_data = json.loads(user_profile.meta or "{}") + except (ValueError, TypeError, AttributeError): + meta_data = {} + extended_profile_model = get_profile_extension_model() if extended_profile_model: try: profile_obj = extended_profile_model.objects.get(user=user_profile.user) - return model_to_dict(profile_obj) + model_data = model_to_dict(profile_obj) + # Model fields take precedence. Meta fills in any field absent from the model. + return {**meta_data, **model_data} except extended_profile_model.DoesNotExist: - return {} + pass - try: - return json.loads(user_profile.meta or "{}") - except (ValueError, TypeError, AttributeError): - return {} + return meta_data data = get_extended_profile_data() field_names = configuration_helpers.get_value("extended_profile_fields", []) From 59b624623df421b8ed7b9c11ff8a78d1b8dc34ad Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 18:57:26 -0500 Subject: [PATCH 05/11] refactor: use ObjectDoesNotExist --- openedx/core/djangoapps/user_api/accounts/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index cc148eb3de46..5d26e60ee4c1 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -613,7 +613,7 @@ def get_extended_profile_data(): model_data = model_to_dict(profile_obj) # Model fields take precedence. Meta fills in any field absent from the model. return {**meta_data, **model_data} - except extended_profile_model.DoesNotExist: + except ObjectDoesNotExist: pass return meta_data From 8d23f46a21a7bf80bfeea2371dfbb48880e12ee5 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Apr 2026 18:59:48 -0500 Subject: [PATCH 06/11] refactor: update tests for extended profile handling and rename functions --- .../accounts/tests/test_serializers.py | 92 +++++++++++++++++-- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_serializers.py b/openedx/core/djangoapps/user_api/accounts/tests/test_serializers.py index 6fac6196aa14..f38407af6be5 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_serializers.py @@ -5,9 +5,9 @@ import logging from unittest.mock import Mock, patch +from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase from django.test.client import RequestFactory -from django.test.utils import override_settings from testfixtures import LogCapture from common.djangoapps.student.models import UserProfile @@ -66,7 +66,7 @@ def setUp(self): self.user_profile = UserProfile.objects.get(user=self.user) @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_from_model(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test getting extended profile from a custom model @@ -96,11 +96,10 @@ def test_get_extended_profile_from_model(self, mock_get_model: Mock, mock_config self.assertIn({"field_name": "title", "field_value": "Software Engineer"}, result) # noqa: PT009 self.assertIn({"field_name": "company", "field_value": "EdX"}, result) # noqa: PT009 - @override_settings(REGISTRATION_EXTENSION_FORM=None) @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - def test_get_extended_profile_model_does_not_exist(self, mock_config_helpers: Mock): + def test_get_extended_profile_no_model_configured_uses_meta(self, mock_config_helpers: Mock): """ - Test fallback to meta field when model instance doesn't exist + Test fallback to meta field when no PROFILE_EXTENSION_FORM is configured. """ mock_config_helpers.get_value.return_value = ["department", "title"] self.user_profile.set_meta({"department": "Sales", "title": "Manager"}) @@ -113,7 +112,80 @@ def test_get_extended_profile_model_does_not_exist(self, mock_config_helpers: Mo self.assertIn({"field_name": "title", "field_value": "Manager"}, result) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") + def test_get_profile_extension_model_instance_missing_falls_back_to_meta( + self, mock_get_model: Mock, mock_config_helpers: Mock + ): + """ + Test that when a model is configured but the user has no record in it, + the function falls back to user_profile.meta instead of returning empty. + """ + mock_config_helpers.get_value.return_value = ["department", "title"] + mock_model = Mock() + mock_model.objects.get.side_effect = ObjectDoesNotExist + mock_get_model.return_value = mock_model + + self.user_profile.set_meta({"department": "Sales", "title": "Manager"}) + self.user_profile.save() + + result = get_extended_profile(self.user_profile) + + self.assertEqual(len(result), 2) # noqa: PT009 + self.assertIn({"field_name": "department", "field_value": "Sales"}, result) # noqa: PT009 + self.assertIn({"field_name": "title", "field_value": "Manager"}, result) # noqa: PT009 + + @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") + def test_get_extended_profile_field_absent_from_model_falls_back_to_meta( + self, mock_get_model: Mock, mock_config_helpers: Mock + ): + """ + Test that a configured field not present in the model is read from meta. + Model has 'department', but 'title' is only in meta and must be returned from there. + """ + mock_config_helpers.get_value.return_value = ["department", "title"] + mock_model = Mock() + mock_get_model.return_value = mock_model + + self.user_profile.set_meta({"department": "Meta-Dept", "title": "Meta-Title"}) + self.user_profile.save() + + with patch("openedx.core.djangoapps.user_api.accounts.serializers.model_to_dict") as mock_model_to_dict: + # Model only knows about 'department', but 'title' is absent. + mock_model_to_dict.return_value = {"department": "Model-Dept"} + + result = get_extended_profile(self.user_profile) + + self.assertEqual(len(result), 2) # noqa: PT009 + self.assertIn({"field_name": "department", "field_value": "Model-Dept"}, result) # noqa: PT009 + self.assertIn({"field_name": "title", "field_value": "Meta-Title"}, result) # noqa: PT009 + + @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") + def test_get_profile_extension_model_takes_precedence_over_meta( + self, mock_get_model: Mock, mock_config_helpers: Mock + ): + """ + Test that when both the model and meta have a value for the same field, + the model value takes precedence. + """ + mock_config_helpers.get_value.return_value = ["department"] + mock_model = Mock() + mock_get_model.return_value = mock_model + + self.user_profile.set_meta({"department": "Meta-Dept"}) + self.user_profile.save() + + with patch("openedx.core.djangoapps.user_api.accounts.serializers.model_to_dict") as mock_model_to_dict: + mock_model_to_dict.return_value = {"department": "Model-Dept"} + + result = get_extended_profile(self.user_profile) + + self.assertEqual(len(result), 1) # noqa: PT009 + self.assertIn({"field_name": "department", "field_value": "Model-Dept"}, result) # noqa: PT009 + + @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_no_model_configured(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test fallback to meta field when no model is configured @@ -131,7 +203,7 @@ def test_get_extended_profile_no_model_configured(self, mock_get_model: Mock, mo self.assertIn({"field_name": "title", "field_value": "Director"}, result) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_empty_meta(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test getting extended profile with empty meta field @@ -148,7 +220,7 @@ def test_get_extended_profile_empty_meta(self, mock_get_model: Mock, mock_config self.assertIn({"field_name": "title", "field_value": ""}, result) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_invalid_json_in_meta(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test getting extended profile with invalid JSON in meta field @@ -165,7 +237,7 @@ def test_get_extended_profile_invalid_json_in_meta(self, mock_get_model: Mock, m self.assertIn({"field_name": "title", "field_value": ""}, result) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_missing_fields(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test getting extended profile when some configured fields are missing @@ -184,7 +256,7 @@ def test_get_extended_profile_missing_fields(self, mock_get_model: Mock, mock_co self.assertIn({"field_name": "location", "field_value": ""}, result) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.serializers.configuration_helpers") - @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.serializers.get_profile_extension_model") def test_get_extended_profile_no_configured_fields(self, mock_get_model: Mock, mock_config_helpers: Mock): """ Test getting extended profile when no fields are configured From 1baeac2ee362ee808f7f18b12f64c6bbd7e7a332 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 16:19:15 -0500 Subject: [PATCH 07/11] refactor: rename registration extension form references to profile extension form in tests --- .../user_api/accounts/tests/test_forms.py | 12 ++-- .../user_authn/views/tests/test_utils.py | 69 +++---------------- 2 files changed, 15 insertions(+), 66 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py index bc2c4c0acd1a..80893bdcd68c 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py @@ -140,7 +140,7 @@ def test_get_extended_profile_form_model_has_no_objects(self, mock_get_model: Mo self.assertIsNone(form) # noqa: PT009 self.assertEqual(errors, {}) # noqa: PT009 - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_registration_extension_form") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") def test_get_extended_profile_form_with_existing_instance(self, mock_get_model: Mock, mock_get_form: Mock): """ @@ -162,7 +162,7 @@ def test_get_extended_profile_form_with_existing_instance(self, mock_get_model: mock_model.objects.get.assert_called_once_with(user=self.user) mock_get_form.assert_called_once_with(data=extended_profile_fields_data, instance=mock_instance) - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_registration_extension_form") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") def test_get_extended_profile_form_without_existing_instance(self, mock_get_model: Mock, mock_get_form: Mock): """ @@ -184,7 +184,7 @@ def test_get_extended_profile_form_without_existing_instance(self, mock_get_mode mock_model.objects.get.assert_called_once_with(user=self.user) mock_get_form.assert_called_once_with(data=extended_profile_fields_data) - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_registration_extension_form") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") def test_get_extended_profile_form_validation_errors(self, mock_get_model: Mock, mock_get_form: Mock): """ @@ -205,10 +205,10 @@ def test_get_extended_profile_form_validation_errors(self, mock_get_model: Mock, self.assertEqual(errors["department"]["user_message"], "This field is required") # noqa: PT009 self.assertEqual(errors["title"]["user_message"], "Invalid value") # noqa: PT009 - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_registration_extension_form") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") def test_get_extended_profile_form_returns_none(self, mock_get_form: Mock): """ - Test when get_registration_extension_form returns None + Test when get_profile_extension_form returns None """ mock_get_form.return_value = None extended_profile_fields_data = {"department": "Engineering"} @@ -219,7 +219,7 @@ def test_get_extended_profile_form_returns_none(self, mock_get_form: Mock): self.assertIsNone(form) # noqa: PT009 self.assertEqual(errors, {}) # noqa: PT009 - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_registration_extension_form") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") def test_get_extended_profile_form_exception_during_creation(self, mock_get_form: Mock): """ Test when an unexpected exception occurs during form creation diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py index cf0feee323c9..0366cba27f9e 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py @@ -209,41 +209,29 @@ def test_get_extended_profile_model_attribute_error(self, mock_logger: Mock, moc mock_logger.warning.assert_called_once() self.assertIn("Could not load extended profile model", str(mock_logger.warning.call_args)) # noqa: PT009 - @override_settings(PROFILE_EXTENSION_FORM=None, REGISTRATION_EXTENSION_FORM="myapp.forms.LegacyForm") - def test_get_extended_profile_model_with_deprecated_setting_returns_none(self): - """ - Test that using REGISTRATION_EXTENSION_FORM returns None (maintains old behavior). - - This ensures backward compatibility: sites using REGISTRATION_EXTENSION_FORM - will NOT get the new model-based profile capabilities. They continue using - the old UserProfile.meta field approach. - """ - result = get_profile_extension_model() - - self.assertIsNone(result) # noqa: PT009 @ddt.ddt -class TestGetRegistrationExtensionForm(TestCase): +class TestGetProfileExtensionForm(TestCase): """ - Tests for get_registration_extension_form function + Tests for get_profile_extension_form function """ @ddt.data(None, "") - def test_get_registration_extension_form_no_setting(self, setting_value: str | None): + def test_get_profile_extension_form_no_setting(self, setting_value: str | None): """ - Test when neither PROFILE_EXTENSION_FORM nor REGISTRATION_EXTENSION_FORM is configured + Test when PROFILE_EXTENSION_FORM is not configured """ - with override_settings(PROFILE_EXTENSION_FORM=setting_value, REGISTRATION_EXTENSION_FORM=setting_value): + with override_settings(PROFILE_EXTENSION_FORM=setting_value): result = get_profile_extension_form() self.assertIsNone(result) # noqa: PT009 @override_settings(PROFILE_EXTENSION_FORM="myapp.forms.CustomProfileForm") @patch("openedx.core.djangoapps.user_authn.views.registration_form.import_module") - def test_get_registration_extension_form_with_new_setting(self, mock_import_module: Mock): + def test_get_profile_extension_form_with_setting(self, mock_import_module: Mock): """ - Test loading form from PROFILE_EXTENSION_FORM (new setting) + Test loading form from PROFILE_EXTENSION_FORM setting """ mock_form_instance = Mock() mock_form_class = Mock(return_value=mock_form_instance) @@ -257,49 +245,10 @@ def test_get_registration_extension_form_with_new_setting(self, mock_import_modu mock_import_module.assert_called_once_with("myapp.forms") mock_form_class.assert_called_once_with(data={"field": "value"}) - @override_settings(PROFILE_EXTENSION_FORM="myapp.forms.NewForm", REGISTRATION_EXTENSION_FORM="myapp.forms.OldForm") - @patch("openedx.core.djangoapps.user_authn.views.registration_form.import_module") - def test_get_registration_extension_form_new_setting_precedence(self, mock_import_module: Mock): - """ - Test that PROFILE_EXTENSION_FORM takes precedence over REGISTRATION_EXTENSION_FORM - """ - mock_form_instance = Mock() - mock_form_class = Mock(return_value=mock_form_instance) - mock_module = Mock() - mock_module.NewForm = mock_form_class - mock_import_module.return_value = mock_module - - result = get_profile_extension_form() - - self.assertEqual(result, mock_form_instance) # noqa: PT009 - mock_import_module.assert_called_once_with("myapp.forms") - - @override_settings(PROFILE_EXTENSION_FORM=None, REGISTRATION_EXTENSION_FORM="myapp.forms.LegacyForm") - @patch("openedx.core.djangoapps.user_authn.views.registration_form.import_module") - @patch("openedx.core.djangoapps.user_authn.views.registration_form.log") - def test_get_registration_extension_form_deprecation_warning(self, mock_logger: Mock, mock_import_module: Mock): - """ - Test that using REGISTRATION_EXTENSION_FORM logs a deprecation warning - """ - mock_form_instance = Mock() - mock_form_class = Mock(return_value=mock_form_instance) - mock_module = Mock() - mock_module.LegacyForm = mock_form_class - mock_import_module.return_value = mock_module - - result = get_profile_extension_form() - - self.assertEqual(result, mock_form_instance) # noqa: PT009 - deprecation_calls = [call for call in mock_logger.warning.call_args_list if "deprecated" in str(call).lower()] - self.assertGreater(len(deprecation_calls), 0, "Expected a deprecation warning to be logged") # noqa: PT009 - warning_message = str(deprecation_calls[0]) - self.assertIn("REGISTRATION_EXTENSION_FORM", warning_message) # noqa: PT009 - self.assertIn("PROFILE_EXTENSION_FORM", warning_message) # noqa: PT009 - @override_settings(PROFILE_EXTENSION_FORM="invalid.path") @patch("openedx.core.djangoapps.user_authn.views.registration_form.import_module") @patch("openedx.core.djangoapps.user_authn.views.registration_form.log") - def test_get_registration_extension_form_import_error(self, mock_logger: Mock, mock_import_module: Mock): + def test_get_profile_extension_form_import_error(self, mock_logger: Mock, mock_import_module: Mock): """ Test when form import fails """ @@ -313,7 +262,7 @@ def test_get_registration_extension_form_import_error(self, mock_logger: Mock, m @override_settings(PROFILE_EXTENSION_FORM="invalid_path_without_dot") @patch("openedx.core.djangoapps.user_authn.views.registration_form.log") - def test_get_registration_extension_form_malformed_path(self, mock_logger: Mock): + def test_get_profile_extension_form_malformed_path(self, mock_logger: Mock): """ Test when setting value doesn't have proper format (no dot separator) """ From 260ac1a50ce1fedd5c74f5fa3600a335db4bf4a0 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 16:23:43 -0500 Subject: [PATCH 08/11] fix: reorder import statements --- openedx/core/djangoapps/user_api/accounts/forms.py | 2 +- openedx/core/djangoapps/user_authn/views/tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/forms.py b/openedx/core/djangoapps/user_api/accounts/forms.py index 6e02ce30e09b..673eca7324e4 100644 --- a/openedx/core/djangoapps/user_api/accounts/forms.py +++ b/openedx/core/djangoapps/user_api/accounts/forms.py @@ -11,8 +11,8 @@ from common.djangoapps.student.models import User from openedx.core.djangoapps.user_api.accounts.utils import handle_retirement_cancellation from openedx.core.djangoapps.user_authn.views.registration_form import ( - get_profile_extension_model, get_profile_extension_form, + get_profile_extension_model, ) logger = logging.getLogger(__name__) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py index 0366cba27f9e..c3d002a00fa2 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_utils.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_utils.py @@ -11,8 +11,8 @@ from django.test.utils import override_settings from openedx.core.djangoapps.user_authn.views.registration_form import ( - get_profile_extension_model, get_profile_extension_form, + get_profile_extension_model, ) from openedx.core.djangoapps.user_authn.views.utils import _get_username_prefix, get_auto_generated_username From dbb482aa64b0a154b8f448e0a0c3d86e49270ae9 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 17:04:28 -0500 Subject: [PATCH 09/11] refactor: update test cases to use get_profile_extension_model instead of get_extended_profile_model --- .../user_api/accounts/tests/test_forms.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py index 80893bdcd68c..565de4b7181a 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py @@ -113,7 +113,7 @@ def setUp(self): super().setUp() self.user = UserFactory.create() - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model") def test_get_extended_profile_form_no_model_configured(self, mock_get_model: Mock): """ Test when no extended profile model is configured @@ -126,7 +126,7 @@ def test_get_extended_profile_form_no_model_configured(self, mock_get_model: Moc self.assertIsNone(form) # noqa: PT009 self.assertEqual(errors, {}) # noqa: PT009 - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model") def test_get_extended_profile_form_model_has_no_objects(self, mock_get_model: Mock): """ Test when model doesn't have objects attribute (AttributeError) @@ -141,7 +141,7 @@ def test_get_extended_profile_form_model_has_no_objects(self, mock_get_model: Mo self.assertEqual(errors, {}) # noqa: PT009 @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model") def test_get_extended_profile_form_with_existing_instance(self, mock_get_model: Mock, mock_get_form: Mock): """ Test form creation with an existing profile instance @@ -163,7 +163,7 @@ def test_get_extended_profile_form_with_existing_instance(self, mock_get_model: mock_get_form.assert_called_once_with(data=extended_profile_fields_data, instance=mock_instance) @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model") def test_get_extended_profile_form_without_existing_instance(self, mock_get_model: Mock, mock_get_form: Mock): """ Test form creation for a new profile (no existing instance) @@ -185,7 +185,7 @@ def test_get_extended_profile_form_without_existing_instance(self, mock_get_mode mock_get_form.assert_called_once_with(data=extended_profile_fields_data) @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_form") - @patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model") + @patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model") def test_get_extended_profile_form_validation_errors(self, mock_get_model: Mock, mock_get_form: Mock): """ Test when form validation fails @@ -213,7 +213,7 @@ def test_get_extended_profile_form_returns_none(self, mock_get_form: Mock): mock_get_form.return_value = None extended_profile_fields_data = {"department": "Engineering"} - with patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model"): + with patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model"): form, errors = get_extended_profile_form(extended_profile_fields_data, self.user) self.assertIsNone(form) # noqa: PT009 @@ -227,7 +227,7 @@ def test_get_extended_profile_form_exception_during_creation(self, mock_get_form mock_get_form.side_effect = Exception("Unexpected error") extended_profile_fields_data = {"department": "Engineering"} - with patch("openedx.core.djangoapps.user_api.accounts.forms.get_extended_profile_model"): + with patch("openedx.core.djangoapps.user_api.accounts.forms.get_profile_extension_model"): form, errors = get_extended_profile_form(extended_profile_fields_data, self.user) self.assertIsNone(form) # noqa: PT009 From 8cfaa23a652e193a4dfc3efaa27b23b23b4c325a Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 12 May 2026 17:45:00 -0500 Subject: [PATCH 10/11] refactor: rename extended profile model references to profile extension model --- openedx/core/djangoapps/user_api/accounts/forms.py | 12 ++++++------ .../core/djangoapps/user_api/accounts/serializers.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/forms.py b/openedx/core/djangoapps/user_api/accounts/forms.py index 673eca7324e4..529fdcbd1a72 100644 --- a/openedx/core/djangoapps/user_api/accounts/forms.py +++ b/openedx/core/djangoapps/user_api/accounts/forms.py @@ -108,22 +108,22 @@ def get_extended_profile_form( - field_errors (dict): Dictionary of validation errors, if any """ field_errors, kwargs = {}, {} - extended_profile_model = get_profile_extension_model() + profile_extension_model = get_profile_extension_model() try: - kwargs["instance"] = extended_profile_model.objects.get(user=user) + kwargs["instance"] = profile_extension_model.objects.get(user=user) except AttributeError: - logger.info("No extended profile model configured") + logger.info("No profile extension model configured") except ObjectDoesNotExist: - logger.info("No existing extended profile found for user %s, creating new instance", user.username) + logger.info("No existing profile extension found for user %s, creating new instance", user.username) try: extended_profile_form = get_profile_extension_form(data=extended_profile_fields_data, **kwargs) except Exception as e: # pylint: disable=broad-exception-caught logger.error("Unexpected error creating custom form for user %s: %s", user.username, str(e)) - field_errors["extended_profile"] = { + field_errors["profile_extension"] = { "developer_message": f"Error creating custom form: {str(e)}", - "user_message": _("There was an error processing the extended profile information"), + "user_message": _("There was an error processing the profile extension information"), } return None, field_errors diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 5d26e60ee4c1..2aa4feb3a0c2 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -606,10 +606,10 @@ def get_extended_profile_data(): except (ValueError, TypeError, AttributeError): meta_data = {} - extended_profile_model = get_profile_extension_model() - if extended_profile_model: + profile_extension_model = get_profile_extension_model() + if profile_extension_model: try: - profile_obj = extended_profile_model.objects.get(user=user_profile.user) + profile_obj = profile_extension_model.objects.get(user=user_profile.user) model_data = model_to_dict(profile_obj) # Model fields take precedence. Meta fills in any field absent from the model. return {**meta_data, **model_data} From e9fe9e198d1dba3216a549cee499c558861289e0 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 13 May 2026 11:02:52 -0500 Subject: [PATCH 11/11] refactor: update test assertions to reflect renaming --- openedx/core/djangoapps/user_api/accounts/tests/test_forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py index 565de4b7181a..ed2a27eb2247 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_forms.py @@ -231,8 +231,8 @@ def test_get_extended_profile_form_exception_during_creation(self, mock_get_form form, errors = get_extended_profile_form(extended_profile_fields_data, self.user) self.assertIsNone(form) # noqa: PT009 - self.assertIn("extended_profile", errors) # noqa: PT009 - self.assertIn("Error creating custom form", errors["extended_profile"]["developer_message"]) # noqa: PT009 + self.assertIn("profile_extension", errors) # noqa: PT009 + self.assertIn("Error creating custom form", errors["profile_extension"]["developer_message"]) # noqa: PT009 class TestValidateAndGetExtendedProfileForm(TestCase):