Unverified Commit dd4d13af by Bill Filler Committed by GitHub

Merge pull request #16611 from edx/afzaledx/WL-1219

Added extended profile fields to the Account settings page.
parents 773b622e f589dc9d
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
import json import json
import logging import logging
import urlparse
from datetime import datetime from datetime import datetime
import urlparse
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
...@@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _ ...@@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django_countries import countries from django_countries import countries
import third_party_auth import third_party_auth
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.models import CommerceConfiguration
...@@ -403,6 +402,65 @@ def _get_form_descriptions(request): ...@@ -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): def _external_auth_intercept(request, mode):
"""Allow external auth to intercept a login/registration request. """Allow external auth to intercept a login/registration request.
...@@ -564,7 +622,8 @@ def account_settings_context(request): ...@@ -564,7 +622,8 @@ def account_settings_context(request):
'disable_courseware_js': True, 'disable_courseware_js': True,
'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_program_listing': ProgramsApiConfig.is_enabled(),
'show_dashboard_tabs': True, 'show_dashboard_tabs': True,
'order_history': user_orders 'order_history': user_orders,
'extended_profile_fields': _get_extended_profile_fields(),
} }
enterprise_customer_name = None enterprise_customer_name = None
......
...@@ -3096,6 +3096,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = { ...@@ -3096,6 +3096,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
"requires_parental_consent", "requires_parental_consent",
"account_privacy", "account_privacy",
"accomplishments_shared", "accomplishments_shared",
"extended_profile",
] ]
} }
......
...@@ -24,7 +24,8 @@ ...@@ -24,7 +24,8 @@
requires_parental_consent: true, requires_parental_consent: true,
profile_image: null, profile_image: null,
accomplishments_shared: false, accomplishments_shared: false,
default_public_account_fields: [] default_public_account_fields: [],
extended_profile: []
}, },
parse: function(response) { parse: function(response) {
......
...@@ -26,14 +26,15 @@ ...@@ -26,14 +26,15 @@
syncLearnerProfileData, syncLearnerProfileData,
enterpriseName, enterpriseName,
enterpriseReadonlyAccountFields, enterpriseReadonlyAccountFields,
edxSupportUrl edxSupportUrl,
extendedProfileFields
) { ) {
var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData, var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField, showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField,
emailFieldView, socialFields, platformData, emailFieldView, socialFields, platformData,
aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView, aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView,
fullNameFieldData, emailFieldData, countryFieldData; fullNameFieldData, emailFieldData, countryFieldData, additionalFields, fieldItem;
$accountSettingsElement = $('.wrapper-account-settings'); $accountSettingsElement = $('.wrapper-account-settings');
...@@ -231,6 +232,37 @@ ...@@ -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 // Add the social link fields
socialFields = { socialFields = {
title: gettext('Social Media Links'), title: gettext('Social Media Links'),
......
...@@ -217,9 +217,10 @@ ...@@ -217,9 +217,10 @@
} }
}, },
saveValue: function() { saveValue: function() {
var attributes = {},
value = '';
if (this.persistChanges === true) { if (this.persistChanges === true) {
var attributes = {}, value = this.fieldValue() ? [{code: this.fieldValue()}] : [];
value = this.fieldValue() ? [{code: this.fieldValue()}] : [];
attributes[this.options.valueAttribute] = value; attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes); this.saveAttributes(attributes);
} }
...@@ -258,6 +259,61 @@ ...@@ -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({ AuthFieldView: FieldViews.LinkFieldView.extend({
fieldTemplate: field_social_link_template, fieldTemplate: field_social_link_template,
className: function() { className: function() {
......
...@@ -43,7 +43,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str ...@@ -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 }, syncLearnerProfileData = ${ bool(sync_learner_profile_data) | n, dump_js_escaped_json },
enterpriseName = '${ enterprise_name | n, js_escaped_string }', enterpriseName = '${ enterprise_name | n, js_escaped_string }',
enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json }, 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( AccountSettingsFactory(
fieldsData, fieldsData,
...@@ -61,7 +62,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str ...@@ -61,7 +62,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
syncLearnerProfileData, syncLearnerProfileData,
enterpriseName, enterpriseName,
enterpriseReadonlyAccountFields, enterpriseReadonlyAccountFields,
edxSupportUrl edxSupportUrl,
extendedProfileFields
); );
</%static:require_module> </%static:require_module>
</%block> </%block>
...@@ -229,6 +229,17 @@ def update_account_settings(requesting_user, update, username=None): ...@@ -229,6 +229,17 @@ def update_account_settings(requesting_user, update, username=None):
existing_user_profile.set_meta(meta) existing_user_profile.set_meta(meta)
existing_user_profile.save() 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: except PreferenceValidationError as err:
raise AccountValidationError(err.preference_errors) raise AccountValidationError(err.preference_errors)
except AccountValidationError as err: except AccountValidationError as err:
......
""" """
Django REST Framework serializers for the User API Accounts sub-application Django REST Framework serializers for the User API Accounts sub-application
""" """
import json
import logging import logging
from rest_framework import serializers from rest_framework import serializers
...@@ -10,6 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -10,6 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from lms.djangoapps.badges.utils import badges_enabled 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 import errors
from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.serializers import ReadOnlyFieldsSerializerMixin from openedx.core.djangoapps.user_api.serializers import ReadOnlyFieldsSerializerMixin
...@@ -112,6 +114,7 @@ class UserReadOnlySerializer(serializers.Serializer): ...@@ -112,6 +114,7 @@ class UserReadOnlySerializer(serializers.Serializer):
"accomplishments_shared": accomplishments_shared, "accomplishments_shared": accomplishments_shared,
"account_privacy": self.configuration.get('default_visibility'), "account_privacy": self.configuration.get('default_visibility'),
"social_links": None, "social_links": None,
"extended_profile_fields": None,
} }
if user_profile: if user_profile:
...@@ -138,6 +141,7 @@ class UserReadOnlySerializer(serializers.Serializer): ...@@ -138,6 +141,7 @@ class UserReadOnlySerializer(serializers.Serializer):
"social_links": SocialLinkSerializer( "social_links": SocialLinkSerializer(
user_profile.social_links.all(), many=True user_profile.social_links.all(), many=True
).data, ).data,
"extended_profile": get_extended_profile(user_profile),
} }
) )
...@@ -327,6 +331,26 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea ...@@ -327,6 +331,26 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
return instance 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): def get_profile_visibility(user_profile, user, configuration=None):
"""Returns the visibility level for the specified user profile.""" """Returns the visibility level for the specified user profile."""
if user_profile.requires_parental_consent(): if user_profile.requires_parental_consent():
......
...@@ -318,6 +318,7 @@ class AccountSettingsOnCreationTest(TestCase): ...@@ -318,6 +318,7 @@ class AccountSettingsOnCreationTest(TestCase):
'language_proficiencies': [], 'language_proficiencies': [],
'account_privacy': PRIVATE_VISIBILITY, 'account_privacy': PRIVATE_VISIBILITY,
'accomplishments_shared': False, 'accomplishments_shared': False,
'extended_profile': [],
}) })
......
...@@ -250,7 +250,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -250,7 +250,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
Verify that all account fields are returned (even those that are not shareable). Verify that all account fields are returned (even those that are not shareable).
""" """
data = response.data data = response.data
self.assertEqual(18, len(data)) self.assertEqual(19, len(data))
self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.username, data["username"])
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
self.assertEqual("US", data["country"]) self.assertEqual("US", data["country"])
...@@ -382,7 +382,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -382,7 +382,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
with self.assertNumQueries(queries): with self.assertNumQueries(queries):
response = self.send_get(self.client) response = self.send_get(self.client)
data = response.data data = response.data
self.assertEqual(18, len(data)) self.assertEqual(19, len(data))
self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.username, data["username"])
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) 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"): for empty_field in ("year_of_birth", "level_of_education", "mailing_address", "bio"):
...@@ -776,7 +776,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -776,7 +776,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
response = self.send_get(client) response = self.send_get(client)
if has_full_access: if has_full_access:
data = response.data data = response.data
self.assertEqual(18, len(data)) self.assertEqual(19, len(data))
self.assertEqual(self.user.username, data["username"]) self.assertEqual(self.user.username, data["username"])
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"]) self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
self.assertEqual(self.user.email, data["email"]) self.assertEqual(self.user.email, data["email"])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment