diff --git a/common/djangoapps/student/migrations/0012_sociallink.py b/common/djangoapps/student/migrations/0012_sociallink.py
new file mode 100644
index 0000000000..9e98824b63
--- /dev/null
+++ b/common/djangoapps/student/migrations/0012_sociallink.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('student', '0011_course_key_field_to_foreign_key'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SocialLink',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('platform', models.CharField(max_length=30)),
+ ('social_link', models.CharField(max_length=100, blank=True)),
+ ('user_profile', models.ForeignKey(related_name='social_links', to='student.UserProfile')),
+ ],
+ ),
+ ]
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 580b00bd70..74a9748260 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -2374,6 +2374,21 @@ class LanguageProficiency(models.Model):
)
+class SocialLink(models.Model): # pylint: disable=model-missing-unicode
+ """
+ Represents a URL connecting a particular social platform to a user's social profile.
+
+ The platforms are listed in the lms/common.py file under SOCIAL_PLATFORMS.
+ Each entry has a display name, a url_stub that describes a required
+ component of the stored URL and an example of a valid URL.
+
+ The stored social_link value must adhere to the form 'https://www.[url_stub][username]'.
+ """
+ user_profile = models.ForeignKey(UserProfile, db_index=True, related_name='social_links')
+ platform = models.CharField(max_length=30)
+ social_link = models.CharField(max_length=100, blank=True)
+
+
class CourseEnrollmentAttribute(models.Model):
"""
Provide additional information about the user's enrollment.
diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py
index 9a0eeda4b6..3a79f57955 100644
--- a/common/test/acceptance/tests/lms/test_account_settings.py
+++ b/common/test/acceptance/tests/lms/test_account_settings.py
@@ -124,6 +124,7 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest):
"""
super(AccountSettingsPageTest, self).setUp()
self.full_name = XSS_INJECTION
+ self.social_link = ''
self.username, self.user_id = self.log_in_as_unique_user(full_name=self.full_name)
self.visit_account_settings_page()
@@ -177,6 +178,14 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest):
'Year of Birth',
'Preferred Language',
]
+ },
+ {
+ 'title': 'Social Media Links',
+ 'fields': [
+ 'Twitter Link',
+ 'Facebook Link',
+ 'LinkedIn Link',
+ ]
}
]
@@ -463,6 +472,18 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest):
actual_events
)
+ def test_social_links_field(self):
+ """
+ Test behaviour of one of the social media links field.
+ """
+ self._test_text_field(
+ u'social_links',
+ u'Twitter Link',
+ self.social_link,
+ u'www.google.com/invalidlink',
+ [u'https://www.twitter.com/edX', self.social_link],
+ )
+
def test_linked_accounts(self):
"""
Test that fields for third party auth providers exist.
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 9a63804cb6..b20d06c969 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -2931,6 +2931,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
'date_joined',
'language_proficiencies',
'bio',
+ 'social_links',
'account_privacy',
# Not an actual field, but used to signal whether badges should be public.
'accomplishments_shared',
@@ -2953,6 +2954,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
"date_joined",
"profile_image",
"language_proficiencies",
+ "social_links",
"name",
"gender",
"goals",
@@ -2965,6 +2967,32 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
]
}
+# The current list of social platforms to be shown to the user.
+#
+# url_stub represents the host URL, it must end with a forward
+# slash and represent the profile at https://www.[url_stub][username]
+#
+# The example will be used as a placeholder in the social link
+# input field as well as in some messaging describing an example of a
+# valid link.
+SOCIAL_PLATFORMS = {
+ 'facebook': {
+ 'display_name': 'Facebook',
+ 'url_stub': 'facebook.com/',
+ 'example': 'https://www.facebook.com/username'
+ },
+ 'twitter': {
+ 'display_name': 'Twitter',
+ 'url_stub': 'twitter.com/',
+ 'example': 'https://www.twitter.com/username'
+ },
+ 'linkedin': {
+ 'display_name': 'LinkedIn',
+ 'url_stub': 'linkedin.com/in/',
+ 'example': 'www.linkedin.com/in/username'
+ }
+}
+
# E-Commerce API Configuration
ECOMMERCE_PUBLIC_URL_ROOT = None
ECOMMERCE_API_URL = None
diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js
index 165e80a66c..d9b1110b28 100644
--- a/lms/static/js/spec/student_account/account_settings_fields_spec.js
+++ b/lms/static/js/spec/student_account/account_settings_fields_spec.js
@@ -1,21 +1,23 @@
define(['backbone',
- 'jquery',
- 'underscore',
- 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
- 'common/js/spec_helpers/template_helpers',
- 'js/views/fields',
- 'js/spec/views/fields_helpers',
- 'js/spec/student_account/account_settings_fields_helpers',
- 'js/student_account/views/account_settings_fields',
- 'js/student_account/models/user_account_model',
- 'string_utils'],
- function(Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers,
+ 'jquery',
+ 'underscore',
+ 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
+ 'common/js/spec_helpers/template_helpers',
+ 'js/student_account/models/user_account_model',
+ 'js/views/fields',
+ 'js/spec/views/fields_helpers',
+ 'js/spec/student_account/account_settings_fields_helpers',
+ 'js/student_account/views/account_settings_fields',
+ 'js/student_account/models/user_account_model',
+ 'string_utils'],
+ function(Backbone, $, _, AjaxHelpers, TemplateHelpers, UserAccountModel, FieldViews, FieldViewsSpecHelpers,
AccountSettingsFieldViewSpecHelpers, AccountSettingsFieldViews) {
'use strict';
describe('edx.AccountSettingsFieldViews', function() {
var requests,
- timerCallback;
+ timerCallback, // eslint-disable-line no-unused-vars
+ data;
beforeEach(function() {
timerCallback = jasmine.createSpy('timerCallback');
@@ -40,7 +42,7 @@ define(['backbone',
view.$('.u-field-value > button').click();
expect(view.$('.u-field-value > button').is(':disabled')).toBe(true);
AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', 'email=legolas%40woodland.middlearth');
- AjaxHelpers.respondWithJson(requests, {'success': 'true'});
+ AjaxHelpers.respondWithJson(requests, {success: 'true'});
FieldViewsSpecHelpers.expectMessageContains(
view,
"We've sent a message to legolas@woodland.middlearth. " +
@@ -130,7 +132,7 @@ define(['backbone',
var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render();
- var data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]};
+ data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]};
view.$(selector).val(data[fieldData.valueAttribute]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
@@ -145,7 +147,7 @@ define(['backbone',
AjaxHelpers.respondWithNoContent(requests);
FieldViewsSpecHelpers.expectMessageContains(view, 'Your changes have been saved.');
- data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]};
+ data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]};
view.$(selector).val(data[fieldData.valueAttribute]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
@@ -173,13 +175,13 @@ define(['backbone',
options: FieldViewsSpecHelpers.SELECT_OPTIONS,
persistChanges: true
});
- fieldData.model.set({'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]});
+ fieldData.model.set({language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]});
var view = new AccountSettingsFieldViews.LanguageProficienciesFieldView(fieldData).render();
expect(view.modelValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]);
- var data = {'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]};
+ data = {language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]};
view.$(selector).val(FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
diff --git a/lms/static/js/spec/student_account/account_settings_view_spec.js b/lms/static/js/spec/student_account/account_settings_view_spec.js
index f171762b53..d97eebc341 100644
--- a/lms/static/js/spec/student_account/account_settings_view_spec.js
+++ b/lms/static/js/spec/student_account/account_settings_view_spec.js
@@ -1,13 +1,13 @@
define(['backbone',
- 'jquery',
- 'underscore',
- 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
- 'common/js/spec_helpers/template_helpers',
- 'js/spec/student_account/helpers',
- 'js/views/fields',
- 'js/student_account/models/user_account_model',
- 'js/student_account/views/account_settings_view'
- ],
+ 'jquery',
+ 'underscore',
+ 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
+ 'common/js/spec_helpers/template_helpers',
+ 'js/spec/student_account/helpers',
+ 'js/views/fields',
+ 'js/student_account/models/user_account_model',
+ 'js/student_account/views/account_settings_view'
+],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, FieldViews, UserAccountModel,
AccountSettingsView) {
'use strict';
diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js
index 23df61438f..a03ac6aaa9 100644
--- a/lms/static/js/spec/student_account/helpers.js
+++ b/lms/static/js/spec/student_account/helpers.js
@@ -65,6 +65,23 @@ define(['underscore'], function(_) {
};
var IMAGE_MAX_BYTES = 1024 * 1024;
var IMAGE_MIN_BYTES = 100;
+ var SOCIAL_PLATFORMS = {
+ facebook: {
+ display_name: 'Facebook',
+ url_stub: 'facebook.com/',
+ example: 'https://www.facebook.com/username'
+ },
+ twitter: {
+ display_name: 'Twitter',
+ url_stub: 'twitter.com/',
+ example: 'https://www.twitter.com/username'
+ },
+ linkedin: {
+ display_name: 'LinkedIn',
+ url_stub: 'linkedin.com/in/',
+ example: 'https://www.linkedin.com/in/username'
+ }
+ };
var DEFAULT_ACCOUNT_SETTINGS_DATA = {
username: 'student',
name: 'Student',
@@ -77,6 +94,7 @@ define(['underscore'], function(_) {
language: 'en-US',
date_joined: 'December 17, 1995 03:24:00',
bio: 'About the student',
+ social_links: [{platform: 'facebook', social_link: 'https://www.facebook.com/edX'}],
language_proficiencies: [{code: '1'}],
profile_image: PROFILE_IMAGE,
accomplishments_shared: false
@@ -170,6 +188,7 @@ define(['underscore'], function(_) {
AUTH_DATA: AUTH_DATA,
IMAGE_MAX_BYTES: IMAGE_MAX_BYTES,
IMAGE_MIN_BYTES: IMAGE_MIN_BYTES,
+ SOCIAL_PLATFORMS: SOCIAL_PLATFORMS,
createAccountSettingsData: createAccountSettingsData,
createUserPreferencesData: createUserPreferencesData,
expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible,
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 4f9293b92f..da40216eda 100644
--- a/lms/static/js/student_account/models/user_account_model.js
+++ b/lms/static/js/student_account/models/user_account_model.js
@@ -19,6 +19,7 @@
mailing_address: '',
year_of_birth: null,
bio: null,
+ social_links: [],
language_proficiencies: [],
requires_parental_consent: true,
profile_image: null,
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 b88a1223da..30e3440bc9 100644
--- a/lms/static/js/student_account/views/account_settings_factory.js
+++ b/lms/static/js/student_account/views/account_settings_factory.js
@@ -19,14 +19,15 @@
accountUserId,
platformName,
contactEmail,
- allowEmailChange
+ allowEmailChange,
+ socialPlatforms
) {
- var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
+ var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField,
- emailFieldView;
+ emailFieldView, socialFields, platformData;
- accountSettingsElement = $('.wrapper-account-settings');
+ $accountSettingsElement = $('.wrapper-account-settings');
userAccountModel = new UserAccountModel();
userAccountModel.url = userAccountsApiUrl;
@@ -64,7 +65,7 @@
aboutSectionsData = [
{
title: gettext('Basic Account Information'),
- subtitle: gettext('These settings include basic information about your account. You can also specify additional information and see your linked social accounts on this page.'), // eslint-disable-line max-len
+ subtitle: gettext('These settings include basic information about your account.'),
fields: [
{
view: new AccountSettingsFieldViews.ReadonlyFieldView({
@@ -191,6 +192,34 @@
}
];
+ // Add the social link fields
+ socialFields = {
+ title: gettext('Social Media Links'),
+ subtitle: gettext('Optionally, link your personal accounts to the social media icons on your edX profile.'), // eslint-disable-line max-len
+ fields: []
+ };
+
+ for (var socialPlatform in socialPlatforms) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len
+ platformData = socialPlatforms[socialPlatform];
+ socialFields.fields.push(
+ {
+ view: new AccountSettingsFieldViews.SocialLinkTextFieldView({
+ model: userAccountModel,
+ title: gettext(platformData.display_name + ' Link'),
+ valueAttribute: 'social_links',
+ helpMessage: gettext(
+ 'Enter your ' + platformData.display_name + ' username or the URL to your ' +
+ platformData.display_name + ' page. Delete the URL to remove the link.'
+ ),
+ platform: socialPlatform,
+ persistChanges: true,
+ placeholder: platformData.example
+ })
+ }
+ );
+ }
+ aboutSectionsData.push(socialFields);
+
// set TimeZoneField to listen to CountryField
getUserField = function(list, search) {
return _.find(list, function(field) {
@@ -266,7 +295,7 @@
accountSettingsView = new AccountSettingsView({
model: userAccountModel,
accountUserId: accountUserId,
- el: accountSettingsElement,
+ el: $accountSettingsElement,
tabSections: {
aboutTabSections: aboutSectionsData,
accountsTabSections: accountsSectionData,
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 88d229a747..77123c66fd 100644
--- a/lms/static/js/student_account/views/account_settings_fields.js
+++ b/lms/static/js/student_account/views/account_settings_fields.js
@@ -224,6 +224,39 @@
}
}
}),
+ SocialLinkTextFieldView: FieldViews.TextFieldView.extend({
+ render: function() {
+ HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({
+ id: this.options.valueAttribute + '_' + this.options.platform,
+ title: this.options.title,
+ value: this.modelValue(),
+ message: this.options.helpMessage,
+ placeholder: this.options.placeholder || ''
+ }));
+ this.delegateEvents();
+ return this;
+ },
+
+ modelValue: function() {
+ var socialLinks = this.model.get(this.options.valueAttribute);
+ for (var i = 0; i < socialLinks.length; i++) { // eslint-disable-line vars-on-top
+ if (socialLinks[i].platform === this.options.platform) {
+ return socialLinks[i].social_link;
+ }
+ }
+ return null;
+ },
+ saveValue: function() {
+ var attributes, value;
+ if (this.persistChanges === true) {
+ attributes = {};
+ value = this.fieldValue() != null ? [{platform: this.options.platform,
+ social_link: 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/static/js/views/fields.js b/lms/static/js/views/fields.js
index 06215c8280..9376d887d2 100644
--- a/lms/static/js/views/fields.js
+++ b/lms/static/js/views/fields.js
@@ -367,7 +367,8 @@
id: this.options.valueAttribute,
title: this.options.title,
value: this.modelValue(),
- message: this.helpMessage
+ message: this.helpMessage,
+ placeholder: this.options.placeholder || ''
}));
this.delegateEvents();
return this;
diff --git a/lms/static/sass/features/_learner-profile.scss b/lms/static/sass/features/_learner-profile.scss
index c5ab800d33..33a1305462 100644
--- a/lms/static/sass/features/_learner-profile.scss
+++ b/lms/static/sass/features/_learner-profile.scss
@@ -195,6 +195,10 @@
.profile-header {
@include padding(0, $baseline*2, $baseline, $baseline*3);
+ @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
+ @include padding(0, $baseline*2, $baseline, $baseline*0.75);
+ }
+
.header {
@extend %t-title4;
@extend %t-ultrastrong;
@@ -221,11 +225,34 @@
}
.profile-section-one-fields {
- margin: 0 $baseline/2;
+ @include margin(0, $baseline/2, 0, $baseline*0.75);
+
+ .social-links {
+ font-size: 2rem;
+ padding-top: $baseline/4;
+
+ & > span {
+ color: $gray-l4;
+ }
+
+ a {
+ .fa-facebook-square {
+ color: $facebook-blue;
+ }
+
+ .fa-twitter-square {
+ color: $twitter-blue;
+ }
+
+ .fa-linkedin-square {
+ color: $linkedin-blue;
+ }
+ }
+ }
.u-field {
@extend %t-weight4;
- @include padding(0, 0, 0, 3px);
+ padding: 0;
color: $base-font-color;
margin-top: $baseline/5;
@@ -297,18 +324,19 @@
.wrapper-profile-section-container-two {
@include float(left);
- width: calc(100% - 360px);
+ @include padding-left($baseline);
+ width: calc(100% - 380px);
max-width: $learner-profile-container-flex; // Switch to map-get($grid-breakpoints,md) for bootstrap
- padding-left: $baseline;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
+ @include padding-left(0);
width: 100%;
margin-top: $baseline;
}
.u-field-textarea {
margin-bottom: ($baseline/2);
- padding: 0 ($baseline*.75) ($baseline*.75) ($baseline/4);
+ @include padding(0, ($baseline*.75), ($baseline*.75), ($baseline/4));
.u-field-header {
position: relative;
diff --git a/lms/static/sass/views/_account-settings.scss b/lms/static/sass/views/_account-settings.scss
index 67a5d4759b..8cbdd9df0c 100644
--- a/lms/static/sass/views/_account-settings.scss
+++ b/lms/static/sass/views/_account-settings.scss
@@ -94,7 +94,7 @@
.account-settings-sections {
.section-header {
- @extend %t-title6;
+ @extend %t-title5;
@extend %t-strong;
padding-top: ($baseline/2)*3;
color: $dark-gray1;
@@ -102,14 +102,13 @@
.section {
background-color: $white;
- margin-top: $baseline;
+ margin: $baseline 5% 0;
border-bottom: 4px solid $m-gray-l4;
.account-settings-header-subtitle {
- font-size: em(18);
+ font-size: em(14);
line-height: normal;
color: $dark-gray;
- padding-top: 20px;
padding-bottom: 10px;
}
@@ -117,6 +116,7 @@
.u-field {
border-bottom: 2px solid $m-gray-l4;
+ padding: $baseline*0.75 0;
.field {
width: 30%;
@@ -301,7 +301,8 @@
.u-field-message {
position: relative;
- padding: 24px 0 0 ($baseline*5);
+ padding: $baseline*0.75 0 0 ($baseline*4);
+ width: 60%;
.u-field-message-notification {
position: absolute;
diff --git a/lms/templates/fields/field_text_account.underscore b/lms/templates/fields/field_text_account.underscore
index 18133ea2eb..0c4f8a1ae1 100644
--- a/lms/templates/fields/field_text_account.underscore
+++ b/lms/templates/fields/field_text_account.underscore
@@ -1,6 +1,6 @@
-
+
diff --git a/lms/templates/student_account/account_settings.html b/lms/templates/student_account/account_settings.html
index 5c92f0577c..de271d908c 100644
--- a/lms/templates/student_account/account_settings.html
+++ b/lms/templates/student_account/account_settings.html
@@ -37,7 +37,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
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 };
+ allowEmailChange = ${ bool(settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']) | n, dump_js_escaped_json },
+ socialPlatforms = ${ settings.SOCIAL_PLATFORMS | n, dump_js_escaped_json };
AccountSettingsFactory(
fieldsData,
@@ -49,7 +50,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
${ user.id | n, dump_js_escaped_json },
platformName,
contactEmail,
- allowEmailChange
+ allowEmailChange,
+ socialPlatforms
);
%static:require_module>
%block>
diff --git a/lms/templates/student_account/account_settings_section.underscore b/lms/templates/student_account/account_settings_section.underscore
index f99d41f278..11bbbfef82 100644
--- a/lms/templates/student_account/account_settings_section.underscore
+++ b/lms/templates/student_account/account_settings_section.underscore
@@ -3,10 +3,10 @@
<% _.each(sections, function(section) { %>
+
<% if (section.subtitle) { %>
<% } %>
-
diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py
index 264106e4dd..1bda1ee67d 100644
--- a/openedx/core/djangoapps/user_api/accounts/api.py
+++ b/openedx/core/djangoapps/user_api/accounts/api.py
@@ -11,7 +11,7 @@ from django.conf import settings
from django.core.validators import validate_email, ValidationError
from django.http import HttpResponseForbidden
from openedx.core.djangoapps.user_api.preferences.api import update_user_preferences
-from openedx.core.djangoapps.user_api.errors import PreferenceValidationError
+from openedx.core.djangoapps.user_api.errors import PreferenceValidationError, AccountValidationError
from student.models import User, UserProfile, Registration
from student import forms as student_forms
@@ -216,7 +216,9 @@ def update_account_settings(requesting_user, update, username=None):
existing_user_profile.save()
except PreferenceValidationError as err:
- raise errors.AccountValidationError(err.preference_errors)
+ raise AccountValidationError(err.preference_errors)
+ except AccountValidationError as err:
+ raise err
except Exception as err:
raise errors.AccountUpdateError(
u"Error thrown when saving account updates: '{}'".format(err.message)
diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py
index 893893ef26..880a17bab1 100644
--- a/openedx/core/djangoapps/user_api/accounts/serializers.py
+++ b/openedx/core/djangoapps/user_api/accounts/serializers.py
@@ -10,15 +10,17 @@ from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from lms.djangoapps.badges.utils import badges_enabled
+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
+from student.models import UserProfile, LanguageProficiency, SocialLink
+
from . import (
NAME_MIN_LENGTH, ACCOUNT_VISIBILITY_PREF_KEY, PRIVATE_VISIBILITY,
ALL_USERS_VISIBILITY,
)
-from openedx.core.djangoapps.user_api.models import UserPreference
-from openedx.core.djangoapps.user_api.serializers import ReadOnlyFieldsSerializerMixin
-from student.models import UserProfile, LanguageProficiency
from .image_helpers import get_profile_image_urls_for_user
-
+from .utils import validate_social_link, format_social_link
PROFILE_IMAGE_KEY_PREFIX = 'image_url'
LOGGER = logging.getLogger(__name__)
@@ -46,6 +48,15 @@ class LanguageProficiencySerializer(serializers.ModelSerializer):
return None
+class SocialLinkSerializer(serializers.ModelSerializer):
+ """
+ Class that serializes the SocialLink model for the UserProfile object.
+ """
+ class Meta(object):
+ model = SocialLink
+ fields = ("platform", "social_link")
+
+
class UserReadOnlySerializer(serializers.Serializer):
"""
Class that serializes the User model and UserProfile model together.
@@ -99,7 +110,8 @@ class UserReadOnlySerializer(serializers.Serializer):
"mailing_address": None,
"requires_parental_consent": None,
"accomplishments_shared": accomplishments_shared,
- "account_privacy": self.configuration.get('default_visibility')
+ "account_privacy": self.configuration.get('default_visibility'),
+ "social_links": None,
}
if user_profile:
@@ -122,7 +134,10 @@ class UserReadOnlySerializer(serializers.Serializer):
),
"mailing_address": user_profile.mailing_address,
"requires_parental_consent": user_profile.requires_parental_consent(),
- "account_privacy": get_profile_visibility(user_profile, user, self.configuration)
+ "account_privacy": get_profile_visibility(user_profile, user, self.configuration),
+ "social_links": SocialLinkSerializer(
+ user_profile.social_links.all(), many=True
+ ).data,
}
)
@@ -168,11 +183,12 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
profile_image = serializers.SerializerMethodField("_get_profile_image")
requires_parental_consent = serializers.SerializerMethodField()
language_proficiencies = LanguageProficiencySerializer(many=True, required=False)
+ social_links = SocialLinkSerializer(many=True, required=False)
class Meta(object):
model = UserProfile
fields = (
- "name", "gender", "goals", "year_of_birth", "level_of_education", "country",
+ "name", "gender", "goals", "year_of_birth", "level_of_education", "country", "social_links",
"mailing_address", "bio", "profile_image", "requires_parental_consent", "language_proficiencies"
)
# Currently no read-only field, but keep this so view code doesn't need to know.
@@ -192,7 +208,15 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
language_proficiencies = [language for language in value]
unique_language_proficiencies = set(language["code"] for language in language_proficiencies)
if len(language_proficiencies) != len(unique_language_proficiencies):
- raise serializers.ValidationError("The language_proficiencies field must consist of unique languages")
+ raise serializers.ValidationError("The language_proficiencies field must consist of unique languages.")
+ return value
+
+ def validate_social_links(self, value):
+ """ Enforce only one entry for a particular social platform. """
+ social_links = [social_link for social_link in value]
+ unique_social_links = set(social_link["platform"] for social_link in social_links)
+ if len(social_links) != len(unique_social_links):
+ raise serializers.ValidationError("The social_links field must consist of unique social platforms.")
return value
def transform_gender(self, user_profile, value): # pylint: disable=unused-argument
@@ -244,20 +268,22 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
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", which we'll update separately
- update_fields = set(self.get_writeable_fields()) - set(["language_proficiencies"])
+ # 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)
- instance.save()
-
- # Now update the related language proficiency
+ # Update the related language proficiency
if language_proficiencies is not None:
instance.language_proficiencies.all().delete()
instance.language_proficiencies.bulk_create([
@@ -265,6 +291,39 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
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(err.message),
+ "user_message": err.message
+ }
+ })
+
+ # 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
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 008ff705eb..e8260a6803 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py
@@ -297,6 +297,7 @@ class AccountSettingsOnCreationTest(TestCase):
'mailing_address': None,
'year_of_birth': None,
'country': None,
+ 'social_links': [],
'bio': None,
'profile_image': {
'has_image': False,
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py b/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py
new file mode 100644
index 0000000000..0b4428d2d9
--- /dev/null
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py
@@ -0,0 +1,52 @@
+""" Unit tests for custom UserProfile properties. """
+
+import ddt
+
+from django.test import TestCase
+from openedx.core.djangolib.testing.utils import skip_unless_lms
+
+from ..utils import validate_social_link, format_social_link
+
+
+@ddt.ddt
+class UserAccountSettingsTest(TestCase):
+ """Unit tests for setting Social Media Links."""
+
+ def setUp(self):
+ super(UserAccountSettingsTest, self).setUp()
+
+ def validate_social_link(self, social_platform, link):
+ """
+ Helper method that returns True if the social link is valid, False if
+ the input link fails validation and will throw an error.
+ """
+ try:
+ validate_social_link(social_platform, link)
+ except ValueError:
+ return False
+ return True
+
+ @ddt.data(
+ ('facebook', 'www.facebook.com/edX', 'https://www.facebook.com/edX', True),
+ ('facebook', 'facebook.com/edX/', 'https://www.facebook.com/edX', True),
+ ('facebook', 'HTTP://facebook.com/edX/', 'https://www.facebook.com/edX', True),
+ ('facebook', 'www.evilwebsite.com/123', None, False),
+ ('twitter', 'https://www.twiter.com/edX/', None, False),
+ ('twitter', 'https://www.twitter.com/edX/123s', None, False),
+ ('twitter', 'twitter.com/edX', 'https://www.twitter.com/edX', True),
+ ('twitter', 'twitter.com/edX?foo=bar', 'https://www.twitter.com/edX', True),
+ ('linkedin', 'www.linkedin.com/harryrein', None, False),
+ ('linkedin', 'www.linkedin.com/in/harryrein-1234', 'https://www.linkedin.com/in/harryrein-1234', True),
+ ('linkedin', 'www.evilwebsite.com/123?www.linkedin.com/edX', None, False),
+ ('linkedin', '', '', True),
+ ('linkedin', None, None, False),
+ )
+ @ddt.unpack
+ @skip_unless_lms
+ def test_social_link_input(self, platform_name, link_input, formatted_link_expected, is_valid_expected):
+ """
+ Verify that social links are correctly validated and formatted.
+ """
+ self.assertEqual(is_valid_expected, self.validate_social_link(platform_name, link_input))
+
+ self.assertEqual(formatted_link_expected, format_social_link(platform_name, link_input))
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 7622d5fa55..59f01afa6a 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py
@@ -222,7 +222,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
Verify that the shareable fields from the account are returned
"""
data = response.data
- self.assertEqual(9, len(data))
+ self.assertEqual(10, len(data))
self.assertEqual(self.user.username, data["username"])
self.assertEqual("US", data["country"])
self._verify_profile_image_data(data, True)
@@ -247,7 +247,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
Verify that all account fields are returned (even those that are not shareable).
"""
data = response.data
- self.assertEqual(17, len(data))
+ self.assertEqual(18, 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"])
@@ -305,7 +305,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user)
- with self.assertNumQueries(19):
+ with self.assertNumQueries(20):
response = self.send_get(self.different_client)
self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY)
@@ -320,7 +320,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user)
- with self.assertNumQueries(19):
+ with self.assertNumQueries(20):
response = self.send_get(self.different_client)
self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY)
@@ -376,7 +376,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
with self.assertNumQueries(queries):
response = self.send_get(self.client)
data = response.data
- self.assertEqual(17, len(data))
+ self.assertEqual(18, 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"):
@@ -395,12 +395,12 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
self.assertEqual(False, data["accomplishments_shared"])
self.client.login(username=self.user.username, password=TEST_PASSWORD)
- verify_get_own_information(17)
+ verify_get_own_information(18)
# Now make sure that the user can get the same information, even if not active
self.user.is_active = False
self.user.save()
- verify_get_own_information(11)
+ verify_get_own_information(12)
def test_get_account_empty_string(self):
"""
@@ -414,7 +414,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
legacy_profile.save()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
- with self.assertNumQueries(17):
+ with self.assertNumQueries(18):
response = self.send_get(self.client)
for empty_field in ("level_of_education", "gender", "country", "bio"):
self.assertIsNone(response.data[empty_field])
@@ -695,7 +695,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
),
(
[{u"code": u"kw"}, {u"code": u"el"}, {u"code": u"kw"}],
- [u'The language_proficiencies field must consist of unique languages']
+ [u'The language_proficiencies field must consist of unique languages.']
),
)
@ddt.unpack
@@ -769,7 +769,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
response = self.send_get(client)
if has_full_access:
data = response.data
- self.assertEqual(17, len(data))
+ self.assertEqual(18, 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"])
diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py
new file mode 100644
index 0000000000..9097c19f0c
--- /dev/null
+++ b/openedx/core/djangoapps/user_api/accounts/utils.py
@@ -0,0 +1,90 @@
+"""
+Utility methods for the account settings.
+"""
+import re
+from urlparse import urlparse
+
+from django.conf import settings
+from django.utils.translation import ugettext as _
+
+
+def validate_social_link(platform_name, new_social_link):
+ """
+ Given a new social link for a user, ensure that the link takes one of the
+ following forms:
+
+ 1) A valid url that comes from the correct social site.
+ 2) A valid username.
+ 3) A blank value.
+ """
+ formatted_social_link = format_social_link(platform_name, new_social_link)
+
+ # Ensure that the new link is valid.
+ if formatted_social_link is None:
+ required_url_stub = settings.SOCIAL_PLATFORMS[platform_name]['url_stub']
+ raise ValueError(_(
+ ' Make sure that you are providing a valid username or a URL that contains "' +
+ required_url_stub + '". To remove the link from your edX profile, leave this field blank.'
+ ))
+
+
+def format_social_link(platform_name, new_social_link):
+ """
+ Given a user's social link, returns a safe absolute url for the social link.
+
+ Returns the following based on the provided new_social_link:
+ 1) Given an empty string, returns ''
+ 1) Given a valid username, return 'https://www.[platform_name_base][username]'
+ 2) Given a valid URL, return 'https://www.[platform_name_base][username]'
+ 3) Given anything unparseable, returns None
+ """
+ # Blank social links should return '' or None as was passed in.
+ if not new_social_link:
+ return new_social_link
+
+ url_stub = settings.SOCIAL_PLATFORMS[platform_name]['url_stub']
+ username = _get_username_from_social_link(platform_name, new_social_link)
+ if not username:
+ return None
+
+ # For security purposes, always build up the url rather than using input from user.
+ return 'https://www.{}{}'.format(url_stub, username)
+
+
+def _get_username_from_social_link(platform_name, new_social_link):
+ """
+ Returns the username given a social link.
+
+ Uses the following logic to parse new_social_link into a username:
+ 1) If an empty string, returns it as the username.
+ 2) Given a URL, attempts to parse the username from the url and return it.
+ 3) Given a non-URL, returns the entire string as username if valid.
+ 4) If no valid username is found, returns None.
+ """
+ # Blank social links should return '' or None as was passed in.
+ if not new_social_link:
+ return new_social_link
+
+ # Parse the social link as if it were a URL.
+ parse_result = urlparse(new_social_link)
+ url_domain_and_path = parse_result[1] + parse_result[2]
+ url_stub = re.escape(settings.SOCIAL_PLATFORMS[platform_name]['url_stub'])
+ username_match = re.search('(www\.)?' + url_stub + '(?P
.*?)[/]?$', url_domain_and_path, re.IGNORECASE)
+ if username_match:
+ username = username_match.group('username')
+ else:
+ username = new_social_link
+
+ # Ensure the username is a valid username.
+ if not _is_valid_social_username(username):
+ return None
+
+ return username
+
+
+def _is_valid_social_username(value):
+ """
+ Given a particular string, returns whether the string can be considered a safe username.
+ A safe username contains only hyphens, underscores or other alphanumerical characters.
+ """
+ return bool(re.match('^[a-zA-Z0-9_-]*$', value))
diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py
index 2ae88d2548..43d1da074f 100644
--- a/openedx/core/djangoapps/user_api/accounts/views.py
+++ b/openedx/core/djangoapps/user_api/accounts/views.py
@@ -109,6 +109,12 @@ class AccountViewSet(ViewSet):
* requires_parental_consent: True if the user is a minor
requiring parental consent.
+ * social_links: Array of social links. Each
+ preference is a JSON object with the following keys:
+
+ * "platform": A particular social platform, ex: 'facebook'
+ * "social_link": The link to the user's profile on the particular platform
+
* username: The username associated with the account.
* year_of_birth: The year the user was born, as an integer, or null.
* account_privacy: The user's setting for sharing her personal
diff --git a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js b/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js
index 3cb95173c7..321b1080bc 100644
--- a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js
+++ b/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js
@@ -103,6 +103,12 @@
});
sectionOneFieldViews = [
+ new LearnerProfileFieldsView.SocialLinkIconsView({
+ model: accountSettingsModel,
+ socialPlatforms: options.social_platforms,
+ ownProfile: options.own_profile
+ }),
+
new FieldsView.DateFieldView({
title: gettext('Joined'),
titleVisible: true,
diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js
index d642e2df34..7544c62553 100644
--- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js
+++ b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_fields_spec.js
@@ -53,6 +53,17 @@ define(
});
};
+ var createSocialLinksView = function(ownProfile, socialPlatformLinks) {
+ var accountSettingsModel = new UserAccountModel();
+ accountSettingsModel.set({social_platforms: socialPlatformLinks});
+
+ return new LearnerProfileFields.SocialLinkIconsView({
+ model: accountSettingsModel,
+ socialPlatforms: ['twitter', 'facebook', 'linkedin'],
+ ownProfile: ownProfile
+ });
+ };
+
var createFakeImageFile = function(size) {
var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf';
return new Blob(
@@ -75,6 +86,7 @@ define(
loadFixtures('learner_profile/fixtures/learner_profile.html');
TemplateHelpers.installTemplate('templates/fields/field_image');
TemplateHelpers.installTemplate('templates/fields/message_banner');
+ TemplateHelpers.installTemplate('learner_profile/templates/social_icons');
});
afterEach(function() {
@@ -291,5 +303,76 @@ define(
expect($('.message-banner').text().trim()).toBe(imageView.errorMessage);
});
});
+
+ describe('SocialLinkIconsView', function() {
+ var socialPlatformLinks,
+ socialLinkData,
+ socialLinksView,
+ socialPlatform,
+ $icon;
+
+ it('icons are visible and links to social profile if added in account settings', function() {
+ socialPlatformLinks = {
+ twitter: {
+ platform: 'twitter',
+ social_link: 'https://www.twitter.com/edX'
+ },
+ facebook: {
+ platform: 'facebook',
+ social_link: 'https://www.facebook.com/edX'
+ },
+ linkedin: {
+ platform: 'linkedin',
+ social_link: ''
+ }
+ };
+
+ socialLinksView = createSocialLinksView(true, socialPlatformLinks);
+
+ // Icons should be present and contain links if defined
+ for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top
+ socialPlatform = Object.keys(socialPlatformLinks)[i];
+ socialLinkData = socialPlatformLinks[socialPlatform];
+ if (socialLinkData.social_link) {
+ // Icons with a social_link value should be displayed with a surrounding link
+ $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
+ expect($icon).toExist();
+ expect($icon.parent().is('a'));
+ } else {
+ // Icons without a social_link value should be displayed without a surrounding link
+ $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
+ expect($icon).toExist();
+ expect(!$icon.parent().is('a'));
+ }
+ }
+ });
+
+ it('icons are not visible on a profile with no links', function() {
+ socialPlatformLinks = {
+ twitter: {
+ platform: 'twitter',
+ social_link: ''
+ },
+ facebook: {
+ platform: 'facebook',
+ social_link: ''
+ },
+ linkedin: {
+ platform: 'linkedin',
+ social_link: ''
+ }
+ };
+
+ socialLinksView = createSocialLinksView(false, socialPlatformLinks);
+
+ // Icons should not be present if not defined on another user's profile
+ for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top
+ socialPlatform = Object.keys(socialPlatformLinks)[i];
+ socialLinkData = socialPlatformLinks[socialPlatform];
+ $icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
+ expect($icon).toBe(null);
+ }
+ });
+ });
});
});
diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js
index 8e9a33d8e4..1d88316497 100644
--- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js
+++ b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js
@@ -81,6 +81,12 @@ define(
});
var sectionOneFieldViews = [
+ new LearnerProfileFields.SocialLinkIconsView({
+ model: accountSettingsModel,
+ socialPlatforms: Helpers.SOCIAL_PLATFORMS,
+ ownProfile: true
+ }),
+
new FieldViews.DropdownFieldView({
title: gettext('Location'),
model: accountSettingsModel,
diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js
index 76022f9901..9f943e5214 100644
--- a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js
+++ b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js
@@ -2,10 +2,17 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
'use strict';
var expectProfileElementContainsField = function(element, view) {
+ var titleElement, fieldTitle;
var $element = $(element);
- var fieldTitle = $element.find('.u-field-title').text().trim();
- if (!_.isUndefined(view.options.title)) {
+ // Avoid testing for elements without titles
+ titleElement = $element.find('.u-field-title');
+ if (titleElement.length === 0) {
+ return;
+ }
+
+ fieldTitle = titleElement.text().trim();
+ if (!_.isUndefined(view.options.title) && !_.isUndefined(fieldTitle)) {
expect(fieldTitle).toBe(view.options.title);
}
@@ -41,9 +48,10 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
};
var expectSectionOneTobeRendered = function(learnerProfileView) {
- var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
+ var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one'))
+ .find('.u-field, .social-links');
- expect(sectionOneFieldElements.length).toBe(6);
+ expect(sectionOneFieldElements.length).toBe(7);
expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView);
expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView);
expectProfileElementContainsField(sectionOneFieldElements[2], learnerProfileView.options.nameFieldView);
diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js
index 96c87ba0e7..ae72da3015 100644
--- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js
+++ b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js
@@ -3,9 +3,17 @@
'use strict';
define([
- 'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/string-utils',
- 'edx-ui-toolkit/js/utils/html-utils', 'js/views/fields', 'js/views/image_field', 'backbone-super'
- ], function(gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView) {
+ 'gettext',
+ 'jquery',
+ 'underscore',
+ 'backbone',
+ 'edx-ui-toolkit/js/utils/string-utils',
+ 'edx-ui-toolkit/js/utils/html-utils',
+ 'js/views/fields',
+ 'js/views/image_field',
+ 'text!learner_profile/templates/social_icons.underscore',
+ 'backbone-super'
+ ], function(gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView, socialIconsTemplate) {
var LearnerProfileFieldViews = {};
LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({
@@ -122,6 +130,31 @@
}
});
+ LearnerProfileFieldViews.SocialLinkIconsView = Backbone.View.extend({
+
+ initialize: function(options) {
+ this.options = _.extend({}, options);
+ },
+
+ render: function() {
+ var socialLinks = {};
+ for (var platformName in this.options.socialPlatforms) { // eslint-disable-line no-restricted-syntax, guard-for-in, vars-on-top, max-len
+ socialLinks[platformName] = null;
+ for (var link in this.model.get('social_links')) { // eslint-disable-line no-restricted-syntax, vars-on-top, max-len
+ if (platformName === this.model.get('social_links')[link].platform) {
+ socialLinks[platformName] = this.model.get('social_links')[link].social_link;
+ }
+ }
+ }
+
+ HtmlUtils.setHtml(this.$el, HtmlUtils.template(socialIconsTemplate)({
+ socialLinks: socialLinks,
+ ownProfile: this.options.ownProfile
+ }));
+ return this;
+ }
+ });
+
return LearnerProfileFieldViews;
});
}).call(this, define || RequireJS.define);
diff --git a/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore b/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore
new file mode 100644
index 0000000000..496dbd60b4
--- /dev/null
+++ b/openedx/features/learner_profile/static/learner_profile/templates/social_icons.underscore
@@ -0,0 +1,9 @@
+
+ <% for (var platform in socialLinks) { %>
+ <% if (socialLinks[platform]) { %>
+
>
+ aria-hidden="true">
+
+ <% } %>
+ <% } %>
+
diff --git a/openedx/features/learner_profile/views.py b/openedx/features/learner_profile/views.py
index d00d5009b4..a8d377bf78 100644
--- a/openedx/features/learner_profile/views.py
+++ b/openedx/features/learner_profile/views.py
@@ -93,6 +93,7 @@ def learner_profile_context(request, profile_username, user_is_staff):
'badges_icon': staticfiles_storage.url('certificates/images/ico-mozillaopenbadges.png'),
'backpack_ui_img': staticfiles_storage.url('certificates/images/backpack-ui.png'),
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
+ 'social_platforms': settings.SOCIAL_PLATFORMS,
},
'disable_courseware_js': True,
}