From f589dc9d29dc8971d00a1859bef3fc744e426032 Mon Sep 17 00:00:00 2001 From: Afzal Wali Date: Wed, 15 Nov 2017 16:50:42 +0500 Subject: [PATCH] WL-1219 Added extended profile fields to the Account settings page. --- lms/djangoapps/student_account/views.py | 65 ++++++++++++++++++- lms/envs/common.py | 1 + .../models/user_account_model.js | 3 +- .../views/account_settings_factory.js | 36 +++++++++- .../views/account_settings_fields.js | 60 ++++++++++++++++- .../student_account/account_settings.html | 6 +- .../core/djangoapps/user_api/accounts/api.py | 11 ++++ .../user_api/accounts/serializers.py | 24 +++++++ .../user_api/accounts/tests/test_api.py | 1 + .../user_api/accounts/tests/test_views.py | 6 +- 10 files changed, 200 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 2358cf8edc..e11626e378 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -2,9 +2,9 @@ import json import logging -import urlparse from datetime import datetime +import urlparse from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model @@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods from django_countries import countries - import third_party_auth from edxmako.shortcuts import render_to_response from lms.djangoapps.commerce.models import CommerceConfiguration @@ -403,6 +402,65 @@ def _get_form_descriptions(request): } +def _get_extended_profile_fields(): + """Retrieve the extended profile fields from site configuration to be shown on the + Account Settings page + + Returns: + A list of dicts. Each dict corresponds to a single field. The keys per field are: + "field_name" : name of the field stored in user_profile.meta + "field_label" : The label of the field. + "field_type" : TextField or ListField + "field_options": a list of tuples for options in the dropdown in case of ListField + """ + + extended_profile_fields = [] + fields_already_showing = ['username', 'name', 'email', 'pref-lang', 'country', 'time_zone', 'level_of_education', + 'gender', 'year_of_birth', 'language_proficiencies', 'social_links'] + + field_labels_map = { + "first_name": _(u"First Name"), + "last_name": _(u"Last Name"), + "city": _(u"City"), + "state": _(u"State/Province/Region"), + "company": _(u"Company"), + "title": _(u"Title"), + "mailing_address": _(u"Mailing address"), + "goals": _(u"Tell us why you're interested in {platform_name}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME) + ), + "profession": _("Profession"), + "specialty": _("Specialty") + } + + extended_profile_field_names = configuration_helpers.get_value('extended_profile_fields', []) + for field_to_exclude in fields_already_showing: + if field_to_exclude in extended_profile_field_names: + extended_profile_field_names.remove(field_to_exclude) # pylint: disable=no-member + + extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', []) + extended_profile_field_option_tuples = {} + for field in extended_profile_field_options.keys(): + field_options = extended_profile_field_options[field] + extended_profile_field_option_tuples[field] = [(option.lower(), option) for option in field_options] + + for field in extended_profile_field_names: + field_dict = { + "field_name": field, + "field_label": field_labels_map.get(field, field), + } + + field_options = extended_profile_field_option_tuples.get(field) + if field_options: + field_dict["field_type"] = "ListField" + field_dict["field_options"] = field_options + else: + field_dict["field_type"] = "TextField" + extended_profile_fields.append(field_dict) + + return extended_profile_fields + + def _external_auth_intercept(request, mode): """Allow external auth to intercept a login/registration request. @@ -564,7 +622,8 @@ def account_settings_context(request): 'disable_courseware_js': True, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, - 'order_history': user_orders + 'order_history': user_orders, + 'extended_profile_fields': _get_extended_profile_fields(), } enterprise_customer_name = None diff --git a/lms/envs/common.py b/lms/envs/common.py index 42898fae2c..ded2f0fa34 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3084,6 +3084,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = { "requires_parental_consent", "account_privacy", "accomplishments_shared", + "extended_profile", ] } diff --git a/lms/static/js/student_account/models/user_account_model.js b/lms/static/js/student_account/models/user_account_model.js index da40216eda..44008ed622 100644 --- a/lms/static/js/student_account/models/user_account_model.js +++ b/lms/static/js/student_account/models/user_account_model.js @@ -24,7 +24,8 @@ requires_parental_consent: true, profile_image: null, accomplishments_shared: false, - default_public_account_fields: [] + default_public_account_fields: [], + extended_profile: [] }, parse: function(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 60ca4be0ef..756c8cb64c 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -26,14 +26,15 @@ syncLearnerProfileData, enterpriseName, enterpriseReadonlyAccountFields, - edxSupportUrl + edxSupportUrl, + extendedProfileFields ) { var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField, emailFieldView, socialFields, platformData, aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView, - fullNameFieldData, emailFieldData, countryFieldData; + fullNameFieldData, emailFieldData, countryFieldData, additionalFields, fieldItem; $accountSettingsElement = $('.wrapper-account-settings'); @@ -231,6 +232,37 @@ } ]; + // Add the extended profile fields + additionalFields = aboutSectionsData[1]; + for (var field in extendedProfileFields) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len + fieldItem = extendedProfileFields[field]; + if (fieldItem.field_type === 'TextField') { + additionalFields.fields.push({ + view: new AccountSettingsFieldViews.ExtendedFieldTextFieldView({ + model: userAccountModel, + title: fieldItem.field_label, + fieldName: fieldItem.field_name, + valueAttribute: 'extended_profile', + persistChanges: true + }) + }); + } else { + if (fieldItem.field_type === 'ListField') { + additionalFields.fields.push({ + view: new AccountSettingsFieldViews.ExtendedFieldListFieldView({ + model: userAccountModel, + title: fieldItem.field_label, + fieldName: fieldItem.field_name, + options: fieldItem.field_options, + valueAttribute: 'extended_profile', + persistChanges: true + }) + }); + } + } + } + + // Add the social link fields socialFields = { title: gettext('Social Media Links'), diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js index 7c83235692..c1a5ec4660 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -217,9 +217,10 @@ } }, saveValue: function() { + var attributes = {}, + value = ''; if (this.persistChanges === true) { - var attributes = {}, - value = this.fieldValue() ? [{code: this.fieldValue()}] : []; + value = this.fieldValue() ? [{code: this.fieldValue()}] : []; attributes[this.options.valueAttribute] = value; this.saveAttributes(attributes); } @@ -258,6 +259,61 @@ } } }), + ExtendedFieldTextFieldView: FieldViews.TextFieldView.extend({ + render: function() { + HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({ + id: this.options.valueAttribute + '_' + this.options.field_name, + title: this.options.title, + value: this.modelValue(), + message: this.options.helpMessage, + placeholder: this.options.placeholder || '' + })); + this.delegateEvents(); + return this; + }, + + modelValue: function() { + var extendedProfileFields = this.model.get(this.options.valueAttribute); + for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top + if (extendedProfileFields[i].field_name === this.options.fieldName) { + return extendedProfileFields[i].field_value; + } + } + return null; + }, + saveValue: function() { + var attributes, value; + if (this.persistChanges === true) { + attributes = {}; + value = this.fieldValue() != null ? [{field_name: this.options.fieldName, + field_value: this.fieldValue()}] : []; + attributes[this.options.valueAttribute] = value; + this.saveAttributes(attributes); + } + } + }), + ExtendedFieldListFieldView: FieldViews.DropdownFieldView.extend({ + fieldTemplate: field_dropdown_account_template, + modelValue: function() { + var extendedProfileFields = this.model.get(this.options.valueAttribute); + for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top + if (extendedProfileFields[i].field_name === this.options.fieldName) { + return extendedProfileFields[i].field_value; + } + } + return null; + }, + saveValue: function() { + var attributes = {}, + value; + if (this.persistChanges === true) { + value = this.fieldValue() ? [{field_name: this.options.fieldName, + field_value: this.fieldValue()}] : []; + attributes[this.options.valueAttribute] = value; + this.saveAttributes(attributes); + } + } + }), AuthFieldView: FieldViews.LinkFieldView.extend({ fieldTemplate: field_social_link_template, className: function() { diff --git a/lms/templates/student_account/account_settings.html b/lms/templates/student_account/account_settings.html index cc21dbcfb1..e82cc333b4 100644 --- a/lms/templates/student_account/account_settings.html +++ b/lms/templates/student_account/account_settings.html @@ -43,7 +43,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str syncLearnerProfileData = ${ bool(sync_learner_profile_data) | n, dump_js_escaped_json }, enterpriseName = '${ enterprise_name | n, js_escaped_string }', enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json }, - edxSupportUrl = '${ edx_support_url | n, js_escaped_string }'; + edxSupportUrl = '${ edx_support_url | n, js_escaped_string }', + extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json }; AccountSettingsFactory( fieldsData, @@ -61,7 +62,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str syncLearnerProfileData, enterpriseName, enterpriseReadonlyAccountFields, - edxSupportUrl + edxSupportUrl, + extendedProfileFields ); diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index b6a847c50d..7a7a8b0b37 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -229,6 +229,17 @@ def update_account_settings(requesting_user, update, username=None): 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 AccountValidationError as err: diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 880a17bab1..5516a1e363 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -1,6 +1,7 @@ """ Django REST Framework serializers for the User API Accounts sub-application """ +import json import logging from rest_framework import serializers @@ -10,6 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from lms.djangoapps.badges.utils import badges_enabled +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_api import errors from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.serializers import ReadOnlyFieldsSerializerMixin @@ -112,6 +114,7 @@ class UserReadOnlySerializer(serializers.Serializer): "accomplishments_shared": accomplishments_shared, "account_privacy": self.configuration.get('default_visibility'), "social_links": None, + "extended_profile_fields": None, } if user_profile: @@ -138,6 +141,7 @@ class UserReadOnlySerializer(serializers.Serializer): "social_links": SocialLinkSerializer( user_profile.social_links.all(), many=True ).data, + "extended_profile": get_extended_profile(user_profile), } ) @@ -327,6 +331,26 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea return instance +def get_extended_profile(user_profile): + """Returns the extended user profile fields stored in user_profile.meta""" + + # pick the keys from the site configuration + extended_profile_field_names = configuration_helpers.get_value('extended_profile_fields', []) + + try: + extended_profile_fields_data = json.loads(user_profile.meta) + except ValueError: + extended_profile_fields_data = {} + + extended_profile = [] + for field_name in extended_profile_field_names: + extended_profile.append({ + "field_name": field_name, + "field_value": extended_profile_fields_data.get(field_name, "") + }) + return extended_profile + + def get_profile_visibility(user_profile, user, configuration=None): """Returns the visibility level for the specified user profile.""" if user_profile.requires_parental_consent(): 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 4e79f7f80c..8301936719 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -318,6 +318,7 @@ class AccountSettingsOnCreationTest(TestCase): 'language_proficiencies': [], 'account_privacy': PRIVATE_VISIBILITY, 'accomplishments_shared': False, + 'extended_profile': [], }) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index c22c118ef1..7e595ca3c4 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -250,7 +250,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): Verify that all account fields are returned (even those that are not shareable). """ data = response.data - self.assertEqual(18, len(data)) + self.assertEqual(19, len(data)) self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) self.assertEqual("US", data["country"]) @@ -382,7 +382,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): with self.assertNumQueries(queries): response = self.send_get(self.client) data = response.data - self.assertEqual(18, len(data)) + self.assertEqual(19, len(data)) self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) for empty_field in ("year_of_birth", "level_of_education", "mailing_address", "bio"): @@ -776,7 +776,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): response = self.send_get(client) if has_full_access: data = response.data - self.assertEqual(18, len(data)) + self.assertEqual(19, len(data)) self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) self.assertEqual(self.user.email, data["email"])