def update(self, instance, validated_data): """ Update the profile, including nested fields. Raises: errors.AccountValidationError: the update was not attempted because validation errors were found with the supplied update """ language_proficiencies = validated_data.pop("language_proficiencies", None) # Update all fields on the user profile that are writeable, # except for "language_proficiencies" and "social_links", which we'll update separately update_fields = set(self.get_writeable_fields()) - set(["language_proficiencies"]) - set(["social_links"]) for field_name in update_fields: default = getattr(instance, field_name) field_value = validated_data.get(field_name, default) setattr(instance, field_name, field_value) # Update the related language proficiency if language_proficiencies is not None: instance.language_proficiencies.all().delete() instance.language_proficiencies.bulk_create([ LanguageProficiency(user_profile=instance, code=language["code"]) for language in language_proficiencies ]) # Update the user's social links social_link_data = self._kwargs['data']['social_links'] if 'social_links' in self._kwargs['data'] else None if social_link_data and len(social_link_data) > 0: new_social_link = social_link_data[0] current_social_links = list(instance.social_links.all()) instance.social_links.all().delete() try: # Add the new social link with correct formatting validate_social_link(new_social_link['platform'], new_social_link['social_link']) formatted_link = format_social_link(new_social_link['platform'], new_social_link['social_link']) instance.social_links.bulk_create([ SocialLink(user_profile=instance, platform=new_social_link['platform'], social_link=formatted_link) ]) except ValueError as err: # If we have encountered any validation errors, return them to the user. raise errors.AccountValidationError({ 'social_links': { "developer_message": u"Error thrown from adding new social link: '{}'".format(text_type(err)), "user_message": text_type(err) } }) # Add back old links unless overridden by new link for current_social_link in current_social_links: if current_social_link.platform != new_social_link['platform']: instance.social_links.bulk_create([ SocialLink(user_profile=instance, platform=current_social_link.platform, social_link=current_social_link.social_link) ]) instance.save() return instance
def _update_social_links(self, instance, requested_social_links): """ Update the given profile instance's social links as requested. """ try: new_social_links = [] deleted_social_platforms = [] for requested_link_data in requested_social_links: requested_platform = requested_link_data['platform'] requested_link_url = requested_link_data['social_link'] validate_social_link(requested_platform, requested_link_url) formatted_link = format_social_link(requested_platform, requested_link_url) if not formatted_link: deleted_social_platforms.append(requested_platform) else: new_social_links.append( SocialLink(user_profile=instance, platform=requested_platform, social_link=formatted_link)) platforms_of_new_social_links = [ s.platform for s in new_social_links ] current_social_links = list(instance.social_links.all()) unreplaced_social_links = [ social_link for social_link in current_social_links if social_link.platform not in platforms_of_new_social_links ] pruned_unreplaced_social_links = [ social_link for social_link in unreplaced_social_links if social_link.platform not in deleted_social_platforms ] merged_social_links = new_social_links + pruned_unreplaced_social_links instance.social_links.all().delete() instance.social_links.bulk_create(merged_social_links) except ValueError as err: # If we have encountered any validation errors, return them to the user. raise errors.AccountValidationError({ 'social_links': { "developer_message": u"Error when adding new social link: '{}'".format( text_type(err)), "user_message": text_type(err) } })
def update_account_settings(requesting_user, update, username=None): """Update user account information. Note: It is up to the caller of this method to enforce the contract that this method is only called with the user who made the request. Arguments: requesting_user (User): The user requesting to modify account information. Only the user with username 'username' has permissions to modify account information. update (dict): The updated account field values. username (str): Optional username specifying which account should be updated. If not specified, `requesting_user.username` is assumed. Raises: errors.UserNotFound: no user with username `username` exists (or `requesting_user.username` if `username` is not specified) errors.UserNotAuthorized: the requesting_user does not have access to change the account associated with `username` errors.AccountValidationError: the update was not attempted because validation errors were found with the supplied update errors.AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the same time, some parts of the update may have been successful, even if an errors.AccountUpdateError is returned; in particular, the user account (not including e-mail address) may have successfully been updated, but then the e-mail change request, which is processed last, may throw an error. errors.UserAPIInternalError: the operation failed due to an unexpected error. """ # Get user if username is None: username = requesting_user.username if requesting_user.username != username: raise errors.UserNotAuthorized() user, user_profile = _get_user_and_profile(username) # Validate fields to update field_errors = {} _validate_read_only_fields(user, update, field_errors) user_serializer = AccountUserSerializer(user, data=update) legacy_profile_serializer = AccountLegacyProfileSerializer(user_profile, data=update) for serializer in user_serializer, legacy_profile_serializer: add_serializer_errors(serializer, update, field_errors) _validate_email_change(user, update, field_errors) _validate_secondary_email(user, update, field_errors) old_name = _validate_name_change(user_profile, update, field_errors) old_language_proficiencies = _get_old_language_proficiencies_if_updating( user_profile, update) if field_errors: raise errors.AccountValidationError(field_errors) # Save requested changes try: for serializer in user_serializer, legacy_profile_serializer: serializer.save() _update_preferences_if_needed(update, requesting_user, user) _notify_language_proficiencies_update_if_needed( update, user, user_profile, old_language_proficiencies) _store_old_name_if_needed(old_name, user_profile, requesting_user) _update_extended_profile_if_needed(update, user_profile) _update_state_if_needed(update, user_profile) except PreferenceValidationError as err: raise AccountValidationError(err.preference_errors) except (AccountUpdateError, AccountValidationError) as err: raise err except Exception as err: raise AccountUpdateError( u"Error thrown when saving account updates: '{}'".format( text_type(err))) _send_email_change_requests_if_needed(update, user)
def update_account_settings(requesting_user, update, username=None): """Update user account information. Note: It is up to the caller of this method to enforce the contract that this method is only called with the user who made the request. Arguments: requesting_user (User): The user requesting to modify account information. Only the user with username 'username' has permissions to modify account information. update (dict): The updated account field values. username (str): Optional username specifying which account should be updated. If not specified, `requesting_user.username` is assumed. Raises: errors.UserNotFound: no user with username `username` exists (or `requesting_user.username` if `username` is not specified) errors.UserNotAuthorized: the requesting_user does not have access to change the account associated with `username` errors.AccountValidationError: the update was not attempted because validation errors were found with the supplied update errors.AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the same time, some parts of the update may have been successful, even if an errors.AccountUpdateError is returned; in particular, the user account (not including e-mail address) may have successfully been updated, but then the e-mail change request, which is processed last, may throw an error. errors.UserAPIInternalError: the operation failed due to an unexpected error. """ if username is None: username = requesting_user.username existing_user, existing_user_profile = _get_user_and_profile(username) if requesting_user.username != username: raise errors.UserNotAuthorized() # If user has requested to change email, we must call the multi-step process to handle this. # It is not handled by the serializer (which considers email to be read-only). changing_email = False if "email" in update: changing_email = True new_email = update["email"] del update["email"] # If user has requested to change name, store old name because we must update associated metadata # after the save process is complete. changing_full_name = False old_name = None if "name" in update: changing_full_name = True old_name = existing_user_profile.name # Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400. read_only_fields = set(update.keys()).intersection( AccountUserSerializer.get_read_only_fields() + AccountLegacyProfileSerializer.get_read_only_fields()) # Build up all field errors, whether read-only, validation, or email errors. field_errors = {} if read_only_fields: for read_only_field in read_only_fields: field_errors[read_only_field] = { "developer_message": u"This field is not editable via this API", "user_message": _(u"The '{field_name}' field cannot be edited.").format( field_name=read_only_field) } del update[read_only_field] user_serializer = AccountUserSerializer(existing_user, data=update) legacy_profile_serializer = AccountLegacyProfileSerializer( existing_user_profile, data=update) for serializer in user_serializer, legacy_profile_serializer: field_errors = add_serializer_errors(serializer, update, field_errors) # If the user asked to change email, validate it. if changing_email: try: student_views.validate_new_email(existing_user, new_email) except ValueError as err: field_errors["email"] = { "developer_message": u"Error thrown from validate_new_email: '{}'".format( text_type(err)), "user_message": text_type(err) } # If the user asked to change full name, validate it if changing_full_name: try: student_forms.validate_name(update['name']) except ValidationError as err: field_errors["name"] = { "developer_message": u"Error thrown from validate_name: '{}'".format(err.message), "user_message": err.message } # If we have encountered any validation errors, return them to the user. if field_errors: raise errors.AccountValidationError(field_errors) try: # If everything validated, go ahead and save the serializers. # We have not found a way using signals to get the language proficiency changes (grouped by user). # As a workaround, store old and new values here and emit them after save is complete. if "language_proficiencies" in update: old_language_proficiencies = list( existing_user_profile.language_proficiencies.values('code')) for serializer in user_serializer, legacy_profile_serializer: serializer.save() # if any exception is raised for user preference (i.e. account_privacy), the entire transaction for user account # patch is rolled back and the data is not saved if 'account_privacy' in update: update_user_preferences( requesting_user, {'account_privacy': update["account_privacy"]}, existing_user) if "language_proficiencies" in update: new_language_proficiencies = update["language_proficiencies"] emit_setting_changed_event( user=existing_user, db_table=existing_user_profile.language_proficiencies.model. _meta.db_table, setting_name="language_proficiencies", old_value=old_language_proficiencies, new_value=new_language_proficiencies, ) # If the name was changed, store information about the change operation. This is outside of the # serializer so that we can store who requested the change. if old_name: meta = existing_user_profile.get_meta() if 'old_names' not in meta: meta['old_names'] = [] meta['old_names'].append([ old_name, u"Name change requested through account API by {0}".format( requesting_user.username), datetime.datetime.now(UTC).isoformat() ]) existing_user_profile.set_meta(meta) existing_user_profile.save() # updating extended user profile info if 'extended_profile' in update: meta = existing_user_profile.get_meta() new_extended_profile = update['extended_profile'] for field in new_extended_profile: field_name = field['field_name'] new_value = field['field_value'] meta[field_name] = new_value existing_user_profile.set_meta(meta) existing_user_profile.save() except PreferenceValidationError as err: raise AccountValidationError(err.preference_errors) except (AccountUpdateError, AccountValidationError) as err: raise err except Exception as err: raise AccountUpdateError( u"Error thrown when saving account updates: '{}'".format( text_type(err))) # And try to send the email change request if necessary. if changing_email: if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']: raise AccountUpdateError( u"Email address changes have been disabled by the site operators." ) try: student_views.do_email_change_request(existing_user, new_email) except ValueError as err: raise AccountUpdateError( u"Error thrown from do_email_change_request: '{}'".format( text_type(err)), user_message=text_type(err))