diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index 1af6ee8a14008e0fc1fa6dc8ddda4c011eef1b43..39318c8ebbc09db78484fbb318fc3329b0fbedb6 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -193,3 +193,7 @@ else: <%def name="get_tech_support_email_address()"><% return get_value('email_from_address', settings.TECH_SUPPORT_EMAIL) %></%def> + +<%def name="get_contact_email_address()"><% + return get_value('email_from_address', settings.CONTACT_EMAIL) +%></%def> diff --git a/lms/envs/common.py b/lms/envs/common.py index bec8589139a4c817c0cee8014d6044212fe3852e..97aaec66cf535495dc7d74b4fd5448dde7a83b5b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -387,6 +387,9 @@ FEATURES = { # Whether to display account activation notification on dashboard. 'DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR': False, + + # Allow users to change their email address. + 'ALLOW_EMAIL_ADDRESS_CHANGE': True, } # Ignore static asset files on import which match this pattern diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js index 95fd417b37ff314f3d8370c33cdc554423bd7707..4b098bf8db6fb8813f0da684f0f37ab2e74899c1 100644 --- a/lms/static/js/spec/student_account/account_settings_factory_spec.js +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -22,7 +22,10 @@ define(['backbone', Helpers.PASSWORD_RESET_SUPPORT_LINK, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL, - Helpers.PLATFORM_NAME + 1, + Helpers.PLATFORM_NAME, + Helpers.CONTACT_EMAIL, + true ); return context.accountSettingsView; }; diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js index 7978f01a2da56f8e8f4a89dc389c0ed65720af38..6660e07b80c81e1b9b2d155e3e2182a1b21ae7f2 100644 --- a/lms/static/js/spec/student_account/helpers.js +++ b/lms/static/js/spec/student_account/helpers.js @@ -9,6 +9,7 @@ define(['underscore'], function(_) { var FIND_COURSES_URL = '/courses'; var PASSWORD_RESET_SUPPORT_LINK = 'https://support.edx.org/hc/en-us/articles/206212088-What-if-I-did-not-receive-a-password-reset-message-'; // eslint-disable-line max-len var PLATFORM_NAME = 'edX'; + var CONTACT_EMAIL = 'info@example.com'; var PROFILE_IMAGE = { image_url_large: '/media/profile-images/image.jpg', has_image: true @@ -160,6 +161,7 @@ define(['underscore'], function(_) { IMAGE_REMOVE_API_URL: IMAGE_REMOVE_API_URL, PASSWORD_RESET_SUPPORT_LINK: PASSWORD_RESET_SUPPORT_LINK, PLATFORM_NAME: PLATFORM_NAME, + CONTACT_EMAIL: CONTACT_EMAIL, PROFILE_IMAGE: PROFILE_IMAGE, FIELD_OPTIONS: FIELD_OPTIONS, TIME_ZONE_RESPONSE: TIME_ZONE_RESPONSE, diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 1fa127e78be3e7dd75325dc315db9b9668f0b294..4e5abcb97039946a86b3e9e6d468b89f31b548f3 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -17,11 +17,14 @@ userAccountsApiUrl, userPreferencesApiUrl, accountUserId, - platformName + platformName, + contactEmail, + allowEmailChange ) { var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, - showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField; + showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField, + emailFieldView; accountSettingsElement = $('.wrapper-account-settings'); @@ -31,6 +34,33 @@ userPreferencesModel = new UserPreferencesModel(); userPreferencesModel.url = userPreferencesApiUrl; + if (allowEmailChange) { + emailFieldView = { + view: new AccountSettingsFieldViews.EmailFieldView({ + model: userAccountModel, + title: gettext('Email Address'), + valueAttribute: 'email', + helpMessage: StringUtils.interpolate( + gettext('The email address you use to sign in. Communications from {platform_name} and your courses are sent to this address.'), // eslint-disable-line max-len + {platform_name: platformName} + ), + persistChanges: true + }) + }; + } else { + emailFieldView = { + view: new AccountSettingsFieldViews.ReadonlyFieldView({ + model: userAccountModel, + title: gettext('Email Address'), + valueAttribute: 'email', + helpMessage: StringUtils.interpolate( + gettext('The email address you use to sign in. Communications from {platform_name} and your courses are sent to this address. To change the email address, please contact {contact_email}.'), // eslint-disable-line max-len + {platform_name: platformName, contact_email: contactEmail} + ) + }) + }; + } + aboutSectionsData = [ { title: gettext('Basic Account Information'), @@ -58,18 +88,7 @@ persistChanges: true }) }, - { - view: new AccountSettingsFieldViews.EmailFieldView({ - model: userAccountModel, - title: gettext('Email Address'), - valueAttribute: 'email', - helpMessage: StringUtils.interpolate( - gettext('The email address you use to sign in. Communications from {platform_name} and your courses are sent to this address.'), // eslint-disable-line max-len - {platform_name: platformName} - ), - persistChanges: true - }) - }, + emailFieldView, { view: new AccountSettingsFieldViews.PasswordFieldView({ model: userAccountModel, diff --git a/lms/static/sass/views/_account-settings.scss b/lms/static/sass/views/_account-settings.scss index 03b7e7922d581fc941bd0e8df6c28bb84bcc94c8..4bb819ccc56340c29bf79df0e6edbe08831535ec 100644 --- a/lms/static/sass/views/_account-settings.scss +++ b/lms/static/sass/views/_account-settings.scss @@ -184,7 +184,9 @@ line-height: normal; } - #u-field-value-username { + // This should only apply for the email field if email address + // changes are disabled, so we explicitly specify "span". + #u-field-value-username, span#u-field-value-email { padding-top: ($baseline/2); } } diff --git a/lms/templates/student_account/account_settings.html b/lms/templates/student_account/account_settings.html index fdfd520aaaf1a3822f9b3aac5b914a18d71a0bc2..5c92f0577cde35b3ae01f24972525a6032ae804e 100644 --- a/lms/templates/student_account/account_settings.html +++ b/lms/templates/student_account/account_settings.html @@ -33,9 +33,11 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str <%block name="js_extra"> <%static:require_module module_name="js/student_account/views/account_settings_factory" class_name="AccountSettingsFactory"> var fieldsData = ${ fields | n, dump_js_escaped_json }, - ordersHistoryData = ${ order_history | n, dump_js_escaped_json }, - authData = ${ auth | n, dump_js_escaped_json }, - platformName = '${ static.get_platform_name() | n, js_escaped_string }'; + ordersHistoryData = ${ order_history | n, dump_js_escaped_json }, + authData = ${ auth | n, dump_js_escaped_json }, + platformName = '${ static.get_platform_name() | n, js_escaped_string }', + contactEmail = '${ static.get_contact_email_address() | n, js_escaped_string }', + allowEmailChange = ${ bool(settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']) | n, dump_js_escaped_json }; AccountSettingsFactory( fieldsData, @@ -45,7 +47,9 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str '${ user_accounts_api_url | n, js_escaped_string }', '${ user_preferences_api_url | n, js_escaped_string }', ${ user.id | n, dump_js_escaped_json }, - platformName + platformName, + contactEmail, + allowEmailChange ); </%static:require_module> </%block> diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index fe9754f88f726bb673fc200db60d94084f131bb1..543a319a7d38d2abe6b1c3c4d1a54cc3b0ce6166 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -231,6 +231,8 @@ def update_account_settings(requesting_user, update, username=None): # 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: 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 21dc1dc92b63d91ded1f04f8812f4ad7e8a8a573..6b646be6e266f82096916cc3c42ab4e373294286 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -193,6 +193,16 @@ class TestAccountApi(UserSettingsEventTestMixin, TestCase): account_settings = get_account_settings(self.default_request)[0] self.assertEqual("Mickey Mouse", account_settings["name"]) + @patch.dict(settings.FEATURES, dict(ALLOW_EMAIL_ADDRESS_CHANGE=False)) + def test_email_changes_disabled(self): + """ + Test that email address changes are rejected when ALLOW_EMAIL_ADDRESS_CHANGE is not set. + """ + disabled_update = {"email": "valid@example.com"} + with self.assertRaises(AccountUpdateError) as context_manager: + update_account_settings(self.user, disabled_update) + self.assertIn("Email address changes have been disabled", context_manager.exception.developer_message) + @patch('openedx.core.djangoapps.user_api.accounts.serializers.AccountUserSerializer.save') def test_serializer_save_fails(self, serializer_save): """