44"""
55
66import datetime
7+ import logging
78import re
9+ from typing import Optional
810
11+ from django import forms
912from django .conf import settings
1013from django .core .exceptions import ObjectDoesNotExist
1114from django .core .validators import ValidationError , validate_email
3639)
3740from openedx .core .djangoapps .user_api .preferences .api import update_user_preferences
3841from openedx .core .djangoapps .user_authn .utils import check_pwned_password
39- from openedx .core .djangoapps .user_authn .views .registration_form import validate_name , validate_username
42+ from openedx .core .djangoapps .user_authn .views .registration_form import (
43+ get_extended_profile_model , get_registration_extension_form , validate_name , validate_username
44+ )
4045from openedx .core .lib .api .view_utils import add_serializer_errors
4146from openedx .features .enterprise_support .utils import get_enterprise_readonly_account_fields
4247from openedx .features .name_affirmation_api .utils import is_name_affirmation_installed
4853 # pylint: disable=import-error
4954 from edx_name_affirmation .name_change_validator import NameChangeValidator
5055
56+ logger = logging .getLogger (__name__ )
57+
5158# Public access point for this function.
5259visible_fields = _visible_fields
5360
@@ -164,6 +171,7 @@ def update_account_settings(requesting_user, update, username=None):
164171
165172 old_name = _validate_name_change (user_profile , update , field_errors )
166173 old_language_proficiencies = _get_old_language_proficiencies_if_updating (user_profile , update )
174+ extended_profile_form = _get_and_validate_extended_profile_form (update , user , field_errors )
167175
168176 if field_errors :
169177 raise errors .AccountValidationError (field_errors )
@@ -176,7 +184,7 @@ def update_account_settings(requesting_user, update, username=None):
176184 _update_preferences_if_needed (update , requesting_user , user )
177185 _notify_language_proficiencies_update_if_needed (update , user , user_profile , old_language_proficiencies )
178186 _store_old_name_if_needed (old_name , user_profile , requesting_user )
179- _update_extended_profile_if_needed (update , user_profile )
187+ _update_extended_profile_if_needed (update , user_profile , extended_profile_form )
180188 _update_state_if_needed (update , user_profile )
181189
182190 except PreferenceValidationError as err :
@@ -191,6 +199,141 @@ def update_account_settings(requesting_user, update, username=None):
191199 _send_email_change_requests_if_needed (update , user )
192200
193201
202+ def _get_and_validate_extended_profile_form (update : dict , user , field_errors : dict ) -> Optional [forms .Form ]:
203+ """
204+ Get and validate the extended profile form if it exists in the update.
205+
206+ Args:
207+ update (dict): The update data containing potential extended_profile fields
208+ user (User): The user instance for whom the extended profile form is being validated
209+ field_errors (dict): Dictionary to collect field validation errors
210+
211+ Returns:
212+ Optional[forms.Form]: The validated extended profile form instance,
213+ or None if no extended profile form is needed
214+ """
215+ extended_profile = update .get ("extended_profile" )
216+ if not extended_profile :
217+ return None
218+
219+ extended_profile_fields_data = _extract_extended_profile_fields_data (extended_profile , field_errors )
220+ if not extended_profile_fields_data :
221+ return None
222+
223+ extended_profile_form = _get_extended_profile_form_instance (extended_profile_fields_data , user , field_errors )
224+ if not extended_profile_form :
225+ return None
226+
227+ _validate_extended_profile_form_and_collect_errors (extended_profile_form , field_errors )
228+
229+ return extended_profile_form
230+
231+
232+ def _extract_extended_profile_fields_data (extended_profile : Optional [list ], field_errors : dict ) -> dict :
233+ """
234+ Extract extended profile fields data from extended_profile structure.
235+
236+ Args:
237+ extended_profile (Optional[list]): List of field data dictionaries
238+ field_errors (dict): Dictionary to collect validation errors
239+
240+ Returns:
241+ dict: Extracted custom fields data
242+ """
243+ if not isinstance (extended_profile , list ):
244+ field_errors ["extended_profile" ] = {
245+ "developer_message" : "extended_profile must be a list" ,
246+ "user_message" : _ ("Invalid extended profile format" ),
247+ }
248+ return {}
249+
250+ extended_profile_fields_data = {}
251+
252+ for field_data in extended_profile :
253+ if not isinstance (field_data , dict ):
254+ logger .warning ("Invalid field_data structure in extended_profile: %s" , field_data )
255+ continue
256+
257+ field_name = field_data .get ("field_name" )
258+ field_value = field_data .get ("field_value" )
259+
260+ if not field_name :
261+ logger .warning ("Missing field_name in extended_profile field_data: %s" , field_data )
262+ continue
263+
264+ if field_value is not None :
265+ extended_profile_fields_data [field_name ] = field_value
266+
267+ return extended_profile_fields_data
268+
269+
270+ def _get_extended_profile_form_instance (
271+ extended_profile_fields_data : dict , user , field_errors : dict
272+ ) -> Optional [forms .Form ]:
273+ """
274+ Get or create an extended profile form instance.
275+
276+ Attempts to create a form instance using the configured `REGISTRATION_EXTENSION_FORM`.
277+ If an extended profile model exists, tries to bind to existing user data or creates
278+ a new instance. Handles import errors and missing configurations gracefully.
279+
280+ Args:
281+ extended_profile_fields_data (dict): Extended profile field data to populate the form
282+ user (User): User instance to associate with the extended profile
283+ field_errors (dict): Dictionary to collect validation errors if form creation fails
284+
285+ Returns:
286+ Optional[forms.Form]: Extended profile form instance with user data, or None if
287+ no extended profile form is configured or creation fails
288+ """
289+ try :
290+ extended_profile_model = get_extended_profile_model ()
291+
292+ kwargs = {}
293+ if not extended_profile_model :
294+ logger .info ("No extended profile model configured" )
295+ else :
296+ try :
297+ kwargs ["instance" ] = extended_profile_model .objects .get (user = user )
298+ except ObjectDoesNotExist :
299+ logger .info ("No existing extended profile found for user %s, creating new instance" , user .username )
300+
301+ extended_profile_form = get_registration_extension_form (data = extended_profile_fields_data , ** kwargs )
302+
303+ return extended_profile_form
304+
305+ except ImportError as e :
306+ logger .warning ("Extended profile model not available: %s" , str (e ))
307+ return None
308+ except Exception as e : # pylint: disable=broad-exception-caught
309+ logger .error ("Unexpected error creating custom form for user %s: %s" , user .username , str (e ))
310+ field_errors ["extended_profile" ] = {
311+ "developer_message" : f"Error creating custom form: { str (e )} " ,
312+ "user_message" : _ ("There was an error processing the extended profile information" ),
313+ }
314+ return None
315+
316+
317+ def _validate_extended_profile_form_and_collect_errors (extended_profile_form : forms .Form , field_errors : dict ) -> None :
318+ """
319+ Validate the extended profile form and collect any validation errors.
320+
321+ Args:
322+ extended_profile_form (forms.Form): The extended profile form to validate
323+ field_errors (dict): Dictionary to collect validation errors
324+ """
325+ if not extended_profile_form .is_valid ():
326+ logger .info ("Extended profile form validation failed with errors: %s" , extended_profile_form .errors )
327+
328+ for field_name , field_errors_list in extended_profile_form .errors .items ():
329+ first_error = field_errors_list [0 ] if field_errors_list else "Unknown error"
330+
331+ field_errors [field_name ] = {
332+ "developer_message" : f"Error in extended profile field { field_name } : { first_error } " ,
333+ "user_message" : str (first_error ),
334+ }
335+
336+
194337def _validate_read_only_fields (user , data , field_errors ):
195338 # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
196339 read_only_fields = set (data .keys ()).intersection (
@@ -346,17 +489,52 @@ def _notify_language_proficiencies_update_if_needed(data, user, user_profile, ol
346489 )
347490
348491
349- def _update_extended_profile_if_needed (data , user_profile ):
350- if 'extended_profile' in data :
492+ def _update_extended_profile_if_needed (
493+ data : dict , user_profile : UserProfile , extended_profile_form : Optional [forms .Form ]
494+ ) -> None :
495+ """
496+ Update the extended profile information if present in the data.
497+
498+ This function handles two types of extended profile updates:
499+ 1. Updates the user profile meta fields with extended_profile data
500+ 2. Saves the extended profile form data to the extended profile model if valid
501+
502+ Args:
503+ data (dict): Dictionary containing the update data, may include 'extended_profile' key
504+ user_profile (UserProfile): The UserProfile instance to update
505+ extended_profile_form (Optional[forms.Form]): The validated extended profile form
506+ containing extended profile data, or None if no extended profile form is provided
507+
508+ Note:
509+ If 'extended_profile' is present in data, the function will:
510+ - Extract field_name and field_value pairs from extended_profile list
511+ - Update the user_profile.meta dictionary with new values
512+ - Save the updated user_profile
513+
514+ If extended_profile_form is provided and valid, the function will:
515+ - Save the form data to the extended profile model
516+ - Associate the model instance with the user if it's a new instance
517+ - Log any errors that occur during the save process
518+ """
519+ if "extended_profile" in data :
351520 meta = user_profile .get_meta ()
352- new_extended_profile = data [' extended_profile' ]
521+ new_extended_profile = data [" extended_profile" ]
353522 for field in new_extended_profile :
354- field_name = field [' field_name' ]
355- new_value = field [' field_value' ]
523+ field_name = field [" field_name" ]
524+ new_value = field [" field_value" ]
356525 meta [field_name ] = new_value
357526 user_profile .set_meta (meta )
358527 user_profile .save ()
359528
529+ if extended_profile_form :
530+ try :
531+ extended_profile_model = extended_profile_form .save (commit = False )
532+ if not hasattr (extended_profile_model , "user" ) or extended_profile_model .user is None :
533+ extended_profile_model .user = user_profile .user
534+ extended_profile_model .save ()
535+ except Exception as e : # pylint: disable=broad-exception-caught
536+ logger .error ("Error saving extended profile model: %s" , e )
537+
360538
361539def _update_state_if_needed (data , user_profile ):
362540 # If the country was changed to something other than US, remove the state.
0 commit comments