Add social links to learner profile.
LEARNER-1859 Added fields to add social links to the user account settings file. Added icons to the user profile when these links are set, only shown when users show their entire profile. Added jasmine tests for account settings and learner profile pages. Added python unit tests to test validation on the user account.
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