diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index d49cfe8367..b925347b82 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -41,6 +41,7 @@ from openedx.core.djangoapps.user_api.errors import ( ) from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences from openedx.core.lib.api.view_utils import add_serializer_errors +from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields from .serializers import ( AccountLegacyProfileSerializer, AccountUserSerializer, @@ -143,6 +144,25 @@ def update_account_settings(requesting_user, update, username=None): if requesting_user.username != username: raise errors.UserNotAuthorized() + # 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( + # Remove email since it is handled separately below when checking for changing_email. + (set(AccountUserSerializer.get_read_only_fields()) - set(["email"])) | + set(AccountLegacyProfileSerializer.get_read_only_fields() or set()) | + get_enterprise_readonly_account_fields(existing_user) + ) + + # 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] + # 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 @@ -163,22 +183,6 @@ def update_account_settings(requesting_user, update, username=None): if "secondary_email" in update: changing_secondary_email = True - # 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) diff --git a/openedx/core/djangoapps/user_api/accounts/settings_views.py b/openedx/core/djangoapps/user_api/accounts/settings_views.py index ea64db1f3c..ff1f3b11bd 100644 --- a/openedx/core/djangoapps/user_api/accounts/settings_views.py +++ b/openedx/core/djangoapps/user_api/accounts/settings_views.py @@ -125,7 +125,7 @@ def account_settings_context(request): 'beta_language': beta_language } - enterprise_customer = get_enterprise_customer_for_learner(site=request.site, user=request.user) + enterprise_customer = get_enterprise_customer_for_learner(user=request.user) update_account_settings_context_for_enterprise(context, enterprise_customer) if third_party_auth.is_enabled(): diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py index 7be20ff0ef..a3d6f6aeea 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -58,6 +58,7 @@ from openedx.core.djangoapps.user_api.errors import ( UserNotFound ) from openedx.core.djangolib.testing.utils import skip_unless_lms +from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory from student.models import PendingEmailChange from student.tests.factories import UserFactory from student.tests.tests import UserSettingsEventTestMixin @@ -69,6 +70,7 @@ def mock_render_to_string(template_name, context): @skip_unless_lms +@ddt.ddt class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, RetirementTestCase): """ These tests specifically cover the parts of the API methods that are not covered by test_views.py. @@ -220,6 +222,31 @@ class TestAccountApi(UserSettingsEventTestMixin, EmailTemplateTagMixin, Retireme with self.assertRaises(AccountUpdateError): update_account_settings(self.user, {"social_links": social_links}) + def test_update_success_for_enterprise(self): + EnterpriseCustomerUserFactory(user_id=self.user.id) + level_of_education = "m" + successful_update = { + "level_of_education": level_of_education, + } + update_account_settings(self.user, successful_update) + account_settings = get_account_settings(self.default_request)[0] + self.assertEqual(level_of_education, account_settings["level_of_education"]) + + @ddt.data( + ("email", "new_email@example.com"), + ("name", "New Name"), + ("country", "New Country"), + ) + @ddt.unpack + def test_update_validation_error_for_enterprise(self, field_name, field_value): + EnterpriseCustomerUserFactory(user_id=self.user.id) + update_data = {field_name: field_value} + + with self.assertRaises(AccountValidationError) as validation_error: + update_account_settings(self.user, update_data) + field_errors = validation_error.exception.field_errors + self.assertEqual("This field is not editable via this API", field_errors[field_name]["developer_message"]) + def test_update_error_validating(self): """Test that AccountValidationError is thrown if incorrect values are supplied.""" with self.assertRaises(AccountValidationError): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 55ea0692ec..8f9bac8e4a 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -165,6 +165,9 @@ class AccountViewSet(ViewSet): * email: Email address for the user. New email addresses must be confirmed via a confirmation email, so GET does not reflect the change until the address has been confirmed. + * secondary_email: A secondary email address for the user. Unlike + the email field, GET will reflect the latest update to this field + even if changes have yet to be confirmed. * gender: One of the following values: * null diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py index ff835be5e2..8fa48190e2 100644 --- a/openedx/features/enterprise_support/api.py +++ b/openedx/features/enterprise_support/api.py @@ -554,7 +554,7 @@ def get_enterprise_learner_data(user): @enterprise_is_enabled(otherwise={}) -def get_enterprise_customer_for_learner(site, user): +def get_enterprise_customer_for_learner(user): """ Return enterprise customer to whom given learner belongs. """ diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py index 817997cb84..feaa789680 100644 --- a/openedx/features/enterprise_support/utils.py +++ b/openedx/features/enterprise_support/utils.py @@ -260,6 +260,13 @@ def update_account_settings_context_for_enterprise(context, enterprise_customer) context.update(enterprise_context) +def get_enterprise_readonly_account_fields(user): + """ + Returns a set of account fields that are read-only for enterprise users. + """ + return set(settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS) if is_enterprise_learner(user) else set() + + def get_enterprise_learner_generic_name(request): """ Get a generic name concatenating the Enterprise Customer name and 'Learner'.