Merge pull request #15826 from edx/HarryRein/LEANER-1859-social-profile-links
LEARNER-1859: social profile links
This commit is contained in:
23
common/djangoapps/student/migrations/0012_sociallink.py
Normal file
23
common/djangoapps/student/migrations/0012_sociallink.py
Normal file
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
mailing_address: '',
|
||||
year_of_birth: null,
|
||||
bio: null,
|
||||
social_links: [],
|
||||
language_proficiencies: [],
|
||||
requires_parental_consent: true,
|
||||
profile_image: null,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="u-field-value field">
|
||||
<label class="u-field-title field-label" for="field-input-<%- id %>"><%- title %></label>
|
||||
<input class="field-input input-text" type="text" id="field-input-<%- id %>" title="Input field for <%- id %>" aria-describedby="u-field-message-help-<%- id %>" name="input" value="<%- value %>" />
|
||||
<input class="field-input input-text" placeholder="<%- placeholder %>" type="text" id="field-input-<%- id %>" title="Input field for <%- id %>" aria-describedby="u-field-message-help-<%- id %>" name="input" value="<%- value %>" />
|
||||
</div>
|
||||
<span class="u-field-message" id="u-field-message-<%- id %>">
|
||||
<span class="u-field-message-notification" aria-live="polite"></span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
</h2>
|
||||
<% _.each(sections, function(section) { %>
|
||||
<div class="section">
|
||||
<h3 class="section-header"><%- gettext(section.title) %></h3>
|
||||
<% if (section.subtitle) { %>
|
||||
<p class="account-settings-header-subtitle"><%- section.subtitle %></p>
|
||||
<% } %>
|
||||
<h3 class="section-header"><%- gettext(section.title) %></h3>
|
||||
<div class="account-settings-section-body <%- tabName %>-section-body">
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
@@ -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"])
|
||||
|
||||
90
openedx/core/djangoapps/user_api/accounts/utils.py
Normal file
90
openedx/core/djangoapps/user_api/accounts/utils.py
Normal file
@@ -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<username>.*?)[/]?$', 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))
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="social-links">
|
||||
<% for (var platform in socialLinks) { %>
|
||||
<% if (socialLinks[platform]) { %>
|
||||
<a target="_blank" href= <%-socialLinks[platform]%>>
|
||||
<span class="icon fa fa-<%-platform%>-square" data-platform=<%-platform%> aria-hidden="true"></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user