55
66
77import datetime
8+ import logging
89import re
10+ from typing import Optional
911
12+ from django import forms
1013from django .conf import settings
1114from django .core .exceptions import ObjectDoesNotExist
1215from django .core .validators import ValidationError , validate_email
3740)
3841from openedx .core .djangoapps .user_api .preferences .api import update_user_preferences
3942from openedx .core .djangoapps .user_authn .utils import check_pwned_password
40- from openedx .core .djangoapps .user_authn .views .registration_form import validate_name , validate_username
43+ from openedx .core .djangoapps .user_authn .views .registration_form import (
44+ get_extended_profile_model , get_registration_extension_form , validate_name , validate_username
45+ )
4146from openedx .core .lib .api .view_utils import add_serializer_errors
4247from openedx .features .enterprise_support .utils import get_enterprise_readonly_account_fields
4348from 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
@@ -155,6 +162,7 @@ def update_account_settings(requesting_user, update, username=None):
155162 _validate_secondary_email (user , update , field_errors )
156163 old_name = _validate_name_change (user_profile , update , field_errors )
157164 old_language_proficiencies = _get_old_language_proficiencies_if_updating (user_profile , update )
165+ extended_profile_form = _get_and_validate_extended_profile_form (update , user , field_errors )
158166
159167 if field_errors :
160168 raise errors .AccountValidationError (field_errors )
@@ -167,7 +175,7 @@ def update_account_settings(requesting_user, update, username=None):
167175 _update_preferences_if_needed (update , requesting_user , user )
168176 _notify_language_proficiencies_update_if_needed (update , user , user_profile , old_language_proficiencies )
169177 _store_old_name_if_needed (old_name , user_profile , requesting_user )
170- _update_extended_profile_if_needed (update , user_profile )
178+ _update_extended_profile_if_needed (update , user_profile , extended_profile_form )
171179 _update_state_if_needed (update , user_profile )
172180
173181 # Allow a plugin to save the updated values
@@ -189,6 +197,141 @@ def update_account_settings(requesting_user, update, username=None):
189197 _send_email_change_requests_if_needed (update , user )
190198
191199
200+ def _get_and_validate_extended_profile_form (update : dict , user , field_errors : dict ) -> Optional [forms .Form ]:
201+ """
202+ Get and validate the extended profile form if it exists in the update.
203+
204+ Args:
205+ update (dict): The update data containing potential extended_profile fields
206+ user (User): The user instance for whom the extended profile form is being validated
207+ field_errors (dict): Dictionary to collect field validation errors
208+
209+ Returns:
210+ Optional[forms.Form]: The validated extended profile form instance,
211+ or None if no extended profile form is needed
212+ """
213+ extended_profile = update .get ("extended_profile" )
214+ if not extended_profile :
215+ return None
216+
217+ extended_profile_fields_data = _extract_extended_profile_fields_data (extended_profile , field_errors )
218+ if not extended_profile_fields_data :
219+ return None
220+
221+ extended_profile_form = _get_extended_profile_form_instance (extended_profile_fields_data , user , field_errors )
222+ if not extended_profile_form :
223+ return None
224+
225+ _validate_extended_profile_form_and_collect_errors (extended_profile_form , field_errors )
226+
227+ return extended_profile_form
228+
229+
230+ def _extract_extended_profile_fields_data (extended_profile : Optional [list ], field_errors : dict ) -> dict :
231+ """
232+ Extract extended profile fields data from extended_profile structure.
233+
234+ Args:
235+ extended_profile (Optional[list]): List of field data dictionaries
236+ field_errors (dict): Dictionary to collect validation errors
237+
238+ Returns:
239+ dict: Extracted custom fields data
240+ """
241+ if not isinstance (extended_profile , list ):
242+ field_errors ["extended_profile" ] = {
243+ "developer_message" : "extended_profile must be a list" ,
244+ "user_message" : _ ("Invalid extended profile format" ),
245+ }
246+ return {}
247+
248+ extended_profile_fields_data = {}
249+
250+ for field_data in extended_profile :
251+ if not isinstance (field_data , dict ):
252+ logger .warning ("Invalid field_data structure in extended_profile: %s" , field_data )
253+ continue
254+
255+ field_name = field_data .get ("field_name" )
256+ field_value = field_data .get ("field_value" )
257+
258+ if not field_name :
259+ logger .warning ("Missing field_name in extended_profile field_data: %s" , field_data )
260+ continue
261+
262+ if field_value is not None :
263+ extended_profile_fields_data [field_name ] = field_value
264+
265+ return extended_profile_fields_data
266+
267+
268+ def _get_extended_profile_form_instance (
269+ extended_profile_fields_data : dict , user , field_errors : dict
270+ ) -> Optional [forms .Form ]:
271+ """
272+ Get or create an extended profile form instance.
273+
274+ Attempts to create a form instance using the configured `REGISTRATION_EXTENSION_FORM`.
275+ If an extended profile model exists, tries to bind to existing user data or creates
276+ a new instance. Handles import errors and missing configurations gracefully.
277+
278+ Args:
279+ extended_profile_fields_data (dict): Extended profile field data to populate the form
280+ user (User): User instance to associate with the extended profile
281+ field_errors (dict): Dictionary to collect validation errors if form creation fails
282+
283+ Returns:
284+ Optional[forms.Form]: Extended profile form instance with user data, or None if
285+ no extended profile form is configured or creation fails
286+ """
287+ try :
288+ extended_profile_model = get_extended_profile_model ()
289+
290+ kwargs = {}
291+ if not extended_profile_model :
292+ logger .info ("No extended profile model configured" )
293+ else :
294+ try :
295+ kwargs ["instance" ] = extended_profile_model .objects .get (user = user )
296+ except ObjectDoesNotExist :
297+ logger .info ("No existing extended profile found for user %s, creating new instance" , user .username )
298+
299+ extended_profile_form = get_registration_extension_form (data = extended_profile_fields_data , ** kwargs )
300+
301+ return extended_profile_form
302+
303+ except ImportError as e :
304+ logger .warning ("Extended profile model not available: %s" , str (e ))
305+ return None
306+ except Exception as e : # pylint: disable=broad-exception-caught
307+ logger .error ("Unexpected error creating custom form for user %s: %s" , user .username , str (e ))
308+ field_errors ["extended_profile" ] = {
309+ "developer_message" : f"Error creating custom form: { str (e )} " ,
310+ "user_message" : _ ("There was an error processing the extended profile information" ),
311+ }
312+ return None
313+
314+
315+ def _validate_extended_profile_form_and_collect_errors (extended_profile_form : forms .Form , field_errors : dict ) -> None :
316+ """
317+ Validate the extended profile form and collect any validation errors.
318+
319+ Args:
320+ extended_profile_form (forms.Form): The extended profile form to validate
321+ field_errors (dict): Dictionary to collect validation errors
322+ """
323+ if not extended_profile_form .is_valid ():
324+ logger .info ("Extended profile form validation failed with errors: %s" , extended_profile_form .errors )
325+
326+ for field_name , field_errors_list in extended_profile_form .errors .items ():
327+ first_error = field_errors_list [0 ] if field_errors_list else "Unknown error"
328+
329+ field_errors [field_name ] = {
330+ "developer_message" : f"Error in extended profile field { field_name } : { first_error } " ,
331+ "user_message" : str (first_error ),
332+ }
333+
334+
192335def _validate_read_only_fields (user , data , field_errors ):
193336 # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
194337 read_only_fields = set (data .keys ()).intersection (
@@ -344,17 +487,52 @@ def _notify_language_proficiencies_update_if_needed(data, user, user_profile, ol
344487 )
345488
346489
347- def _update_extended_profile_if_needed (data , user_profile ):
348- if 'extended_profile' in data :
490+ def _update_extended_profile_if_needed (
491+ data : dict , user_profile : UserProfile , extended_profile_form : Optional [forms .Form ]
492+ ) -> None :
493+ """
494+ Update the extended profile information if present in the data.
495+
496+ This function handles two types of extended profile updates:
497+ 1. Updates the user profile meta fields with extended_profile data
498+ 2. Saves the extended profile form data to the extended profile model if valid
499+
500+ Args:
501+ data (dict): Dictionary containing the update data, may include 'extended_profile' key
502+ user_profile (UserProfile): The UserProfile instance to update
503+ extended_profile_form (Optional[forms.Form]): The validated extended profile form
504+ containing extended profile data, or None if no extended profile form is provided
505+
506+ Note:
507+ If 'extended_profile' is present in data, the function will:
508+ - Extract field_name and field_value pairs from extended_profile list
509+ - Update the user_profile.meta dictionary with new values
510+ - Save the updated user_profile
511+
512+ If extended_profile_form is provided and valid, the function will:
513+ - Save the form data to the extended profile model
514+ - Associate the model instance with the user if it's a new instance
515+ - Log any errors that occur during the save process
516+ """
517+ if "extended_profile" in data :
349518 meta = user_profile .get_meta ()
350- new_extended_profile = data [' extended_profile' ]
519+ new_extended_profile = data [" extended_profile" ]
351520 for field in new_extended_profile :
352- field_name = field [' field_name' ]
353- new_value = field [' field_value' ]
521+ field_name = field [" field_name" ]
522+ new_value = field [" field_value" ]
354523 meta [field_name ] = new_value
355524 user_profile .set_meta (meta )
356525 user_profile .save ()
357526
527+ if extended_profile_form :
528+ try :
529+ extended_profile_model = extended_profile_form .save (commit = False )
530+ if not hasattr (extended_profile_model , "user" ) or extended_profile_model .user is None :
531+ extended_profile_model .user = user_profile .user
532+ extended_profile_model .save ()
533+ except Exception as e : # pylint: disable=broad-exception-caught
534+ logger .error ("Error saving extended profile model: %s" , e )
535+
358536
359537def _update_state_if_needed (data , user_profile ):
360538 # If the country was changed to something other than US, remove the state.
0 commit comments