feat!: Legacy account, profile, order history removal (#36219)
* feat!: Legacy account, profile, order history removal This removes the legacy account and profile applications, and the order history page. This is primarily a reapplication of #31893, which was rolled back due to prior blockers. FIXES: APER-3884 FIXES: openedx/public-engineering#71 Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com> Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
Learner Profile
|
||||
---------------
|
||||
|
||||
This directory contains a Django application that provides a view to render
|
||||
a profile for any Open edX learner. See `Exploring Your Dashboard and Profile`_
|
||||
for more details.
|
||||
|
||||
.. _Exploring Your Dashboard and Profile: https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/SFD_dashboard_profile_SectionHead.html?highlight=profile
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
<div class="wrapper-profile">
|
||||
<div class="profile profile-other">
|
||||
<div class="wrapper-profile-field-account-privacy"></div>
|
||||
<div class="wrapper-profile-sections account-settings-container">
|
||||
<div class="wrapper-profile-section-container-one">
|
||||
<div class="wrapper-profile-section-one">
|
||||
<div class="profile-image-field">
|
||||
</div>
|
||||
<div class="profile-section-one-fields">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">An error occurred. Try loading the page again.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-two">
|
||||
<div class="wrapper-profile-bio">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-indicator">
|
||||
<p>
|
||||
<span class="spin">
|
||||
<span class="icon fa fa-refresh" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="copy">
|
||||
Loading
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">
|
||||
An error occurred. Please reload the page.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,219 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'gettext',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'logger',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'edx-ui-toolkit/js/pagination/paging-collection',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'js/views/fields',
|
||||
'learner_profile/js/views/learner_profile_fields',
|
||||
'learner_profile/js/views/learner_profile_view',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/views/message_banner',
|
||||
'string_utils'
|
||||
], function(gettext, $, _, Backbone, Logger, StringUtils, PagingCollection, AccountSettingsModel,
|
||||
AccountPreferencesModel, FieldsView, LearnerProfileFieldsView, LearnerProfileView,
|
||||
AccountSettingsFieldViews, MessageBannerView) {
|
||||
return function(options) {
|
||||
var $learnerProfileElement = $('.wrapper-profile');
|
||||
|
||||
var accountSettingsModel = new AccountSettingsModel(
|
||||
_.extend(
|
||||
options.account_settings_data,
|
||||
{
|
||||
default_public_account_fields: options.default_public_account_fields,
|
||||
parental_consent_age_limit: options.parental_consent_age_limit,
|
||||
enable_coppa_compliance: options.enable_coppa_compliance
|
||||
}
|
||||
),
|
||||
{parse: true}
|
||||
);
|
||||
var AccountPreferencesModelWithDefaults = AccountPreferencesModel.extend({
|
||||
defaults: {
|
||||
account_privacy: options.default_visibility
|
||||
}
|
||||
});
|
||||
var accountPreferencesModel = new AccountPreferencesModelWithDefaults(options.preferences_data);
|
||||
|
||||
var editable = options.own_profile ? 'toggle' : 'never';
|
||||
|
||||
var messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
var accountPrivacyFieldView,
|
||||
profileImageFieldView,
|
||||
usernameFieldView,
|
||||
nameFieldView,
|
||||
sectionOneFieldViews,
|
||||
sectionTwoFieldViews,
|
||||
learnerProfileView,
|
||||
getProfileVisibility,
|
||||
showLearnerProfileView;
|
||||
|
||||
accountSettingsModel.url = options.accounts_api_url;
|
||||
accountPreferencesModel.url = options.preferences_api_url;
|
||||
|
||||
accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
|
||||
model: accountPreferencesModel,
|
||||
required: true,
|
||||
editable: 'always',
|
||||
showMessages: false,
|
||||
title: gettext('Profile Visibility:'),
|
||||
valueAttribute: 'account_privacy',
|
||||
options: [
|
||||
['private', gettext('Limited Profile')],
|
||||
['all_users', gettext('Full Profile')]
|
||||
],
|
||||
helpMessage: '',
|
||||
accountSettingsPageUrl: options.account_settings_page_url,
|
||||
persistChanges: true
|
||||
});
|
||||
|
||||
profileImageFieldView = new LearnerProfileFieldsView.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'profile_image',
|
||||
editable: editable === 'toggle',
|
||||
messageView: messageView,
|
||||
imageMaxBytes: options.profile_image_max_bytes,
|
||||
imageMinBytes: options.profile_image_min_bytes,
|
||||
imageUploadUrl: options.profile_image_upload_url,
|
||||
imageRemoveUrl: options.profile_image_remove_url
|
||||
});
|
||||
|
||||
usernameFieldView = new FieldsView.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Username'),
|
||||
valueAttribute: 'username',
|
||||
helpMessage: ''
|
||||
});
|
||||
|
||||
nameFieldView = new FieldsView.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Full Name'),
|
||||
valueAttribute: 'name',
|
||||
helpMessage: ''
|
||||
});
|
||||
|
||||
sectionOneFieldViews = [
|
||||
new LearnerProfileFieldsView.SocialLinkIconsView({
|
||||
model: accountSettingsModel,
|
||||
socialPlatforms: options.social_platforms,
|
||||
ownProfile: options.own_profile
|
||||
}),
|
||||
|
||||
new FieldsView.DateFieldView({
|
||||
title: gettext('Joined'),
|
||||
titleVisible: true,
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Joined Date'),
|
||||
valueAttribute: 'date_joined',
|
||||
helpMessage: '',
|
||||
userLanguage: accountSettingsModel.get('language'),
|
||||
userTimezone: accountPreferencesModel.get('time_zone'),
|
||||
dateFormat: 'MMMM YYYY' // not localized, but hopefully ok.
|
||||
}),
|
||||
|
||||
new FieldsView.DropdownFieldView({
|
||||
title: gettext('Location'),
|
||||
titleVisible: true,
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Country'),
|
||||
required: true,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
placeholderValue: gettext('Add Country'),
|
||||
valueAttribute: 'country',
|
||||
options: options.country_options,
|
||||
helpMessage: '',
|
||||
persistChanges: true
|
||||
}),
|
||||
|
||||
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
|
||||
title: gettext('Language'),
|
||||
titleVisible: true,
|
||||
model: accountSettingsModel,
|
||||
screenReaderTitle: gettext('Preferred Language'),
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
placeholderValue: gettext('Add language'),
|
||||
valueAttribute: 'language_proficiencies',
|
||||
options: options.language_options,
|
||||
helpMessage: '',
|
||||
persistChanges: true
|
||||
})
|
||||
];
|
||||
|
||||
sectionTwoFieldViews = [
|
||||
new FieldsView.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: gettext('About me'),
|
||||
// eslint-disable-next-line max-len
|
||||
placeholderValue: gettext("Tell other learners a little about yourself: where you live, what your interests are, why you're taking courses, or what you hope to learn."),
|
||||
valueAttribute: 'bio',
|
||||
helpMessage: '',
|
||||
persistChanges: true,
|
||||
messagePosition: 'header',
|
||||
maxCharacters: 300
|
||||
})
|
||||
];
|
||||
|
||||
learnerProfileView = new LearnerProfileView({
|
||||
el: $learnerProfileElement,
|
||||
ownProfile: options.own_profile,
|
||||
has_preferences_access: options.has_preferences_access,
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
preferencesModel: accountPreferencesModel,
|
||||
accountPrivacyFieldView: accountPrivacyFieldView,
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
usernameFieldView: usernameFieldView,
|
||||
nameFieldView: nameFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews,
|
||||
platformName: options.platform_name
|
||||
});
|
||||
|
||||
getProfileVisibility = function() {
|
||||
if (options.has_preferences_access) {
|
||||
return accountPreferencesModel.get('account_privacy');
|
||||
} else {
|
||||
return accountSettingsModel.get('profile_is_public') ? 'all_users' : 'private';
|
||||
}
|
||||
};
|
||||
|
||||
showLearnerProfileView = function() {
|
||||
// Record that the profile page was viewed
|
||||
Logger.log('edx.user.settings.viewed', {
|
||||
page: 'profile',
|
||||
visibility: getProfileVisibility(),
|
||||
user_id: options.profile_user_id
|
||||
});
|
||||
|
||||
// Render the view for the first time
|
||||
learnerProfileView.render();
|
||||
};
|
||||
|
||||
if (options.has_preferences_access) {
|
||||
if (accountSettingsModel.get('requires_parental_consent')) {
|
||||
accountPreferencesModel.set('account_privacy', 'private');
|
||||
}
|
||||
}
|
||||
showLearnerProfileView();
|
||||
|
||||
return {
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
accountPreferencesModel: accountPreferencesModel,
|
||||
learnerProfileView: learnerProfileView,
|
||||
};
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -1,79 +0,0 @@
|
||||
define(
|
||||
[
|
||||
'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'learner_profile/js/spec_helpers/helpers',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'learner_profile/js/views/learner_profile_view',
|
||||
'learner_profile/js/views/learner_profile_fields',
|
||||
'learner_profile/js/learner_profile_factory',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
|
||||
UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
|
||||
'use strict';
|
||||
|
||||
describe('edx.user.LearnerProfileFactory', function() {
|
||||
var createProfilePage;
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('learner_profile/fixtures/learner_profile.html');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
createProfilePage = function(ownProfile, options) {
|
||||
return new LearnerProfilePage({
|
||||
accounts_api_url: Helpers.USER_ACCOUNTS_API_URL,
|
||||
preferences_api_url: Helpers.USER_PREFERENCES_API_URL,
|
||||
own_profile: ownProfile,
|
||||
account_settings_page_url: Helpers.USER_ACCOUNTS_API_URL,
|
||||
country_options: Helpers.FIELD_OPTIONS,
|
||||
language_options: Helpers.FIELD_OPTIONS,
|
||||
has_preferences_access: true,
|
||||
profile_image_max_bytes: Helpers.IMAGE_MAX_BYTES,
|
||||
profile_image_min_bytes: Helpers.IMAGE_MIN_BYTES,
|
||||
profile_image_upload_url: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
profile_image_remove_url: Helpers.IMAGE_REMOVE_API_URL,
|
||||
default_visibility: 'all_users',
|
||||
platform_name: 'edX',
|
||||
find_courses_url: '/courses/',
|
||||
account_settings_data: Helpers.createAccountSettingsData(options),
|
||||
preferences_data: Helpers.createUserPreferencesData()
|
||||
});
|
||||
};
|
||||
|
||||
it('renders the full profile for a user', function() {
|
||||
var context,
|
||||
learnerProfileView;
|
||||
AjaxHelpers.requests(this);
|
||||
context = createProfilePage(true);
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
// sets the profile for full view.
|
||||
context.accountPreferencesModel.set({account_privacy: 'all_users'});
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, false);
|
||||
});
|
||||
|
||||
it("renders the limited profile for undefined 'year_of_birth'", function() {
|
||||
var context = createProfilePage(true, {year_of_birth: '', requires_parental_consent: true}),
|
||||
learnerProfileView = context.learnerProfileView;
|
||||
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it('renders the limited profile for under 13 users', function() {
|
||||
var context = createProfilePage(
|
||||
true,
|
||||
{year_of_birth: new Date().getFullYear() - 10, requires_parental_consent: true}
|
||||
);
|
||||
var learnerProfileView = context.learnerProfileView;
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,381 +0,0 @@
|
||||
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/student_account/models/user_account_model',
|
||||
'learner_profile/js/views/learner_profile_fields',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields,
|
||||
MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe('edx.user.LearnerProfileFields', function() {
|
||||
var MOCK_YEAR_OF_BIRTH = 1989;
|
||||
var MOCK_IMAGE_MAX_BYTES = 64;
|
||||
var MOCK_IMAGE_MIN_BYTES = 16;
|
||||
|
||||
var createImageView = function(options) {
|
||||
var yearOfBirth = _.isUndefined(options.yearOfBirth) ? MOCK_YEAR_OF_BIRTH : options.yearOfBirth;
|
||||
var imageMaxBytes = _.isUndefined(options.imageMaxBytes) ? MOCK_IMAGE_MAX_BYTES : options.imageMaxBytes;
|
||||
var imageMinBytes = _.isUndefined(options.imageMinBytes) ? MOCK_IMAGE_MIN_BYTES : options.imageMinBytes;
|
||||
var messageView;
|
||||
|
||||
var imageData = {
|
||||
image_url_large: '/media/profile-images/default.jpg',
|
||||
has_image: !!options.hasImage
|
||||
};
|
||||
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set({profile_image: imageData});
|
||||
accountSettingsModel.set({year_of_birth: yearOfBirth});
|
||||
accountSettingsModel.set({requires_parental_consent: !!_.isEmpty(yearOfBirth)});
|
||||
|
||||
accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL;
|
||||
|
||||
messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
return new LearnerProfileFields.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'profile_image',
|
||||
editable: options.ownProfile,
|
||||
messageView: messageView,
|
||||
imageMaxBytes: imageMaxBytes,
|
||||
imageMinBytes: imageMinBytes,
|
||||
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
|
||||
});
|
||||
};
|
||||
|
||||
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(
|
||||
[fileFakeData.substr(0, size)],
|
||||
{type: 'image/jpg'}
|
||||
);
|
||||
};
|
||||
|
||||
var initializeUploader = function(view) {
|
||||
view.$('.upload-button-input').fileupload({
|
||||
url: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
type: 'POST',
|
||||
add: view.fileSelected,
|
||||
done: view.imageChangeSucceeded,
|
||||
fail: view.imageChangeFailed
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
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() {
|
||||
// image_field.js's window.onBeforeUnload breaks Karma in Chrome, clean it up after each test
|
||||
$(window).off('beforeunload');
|
||||
});
|
||||
|
||||
describe('ProfileImageFieldView', function() {
|
||||
var verifyImageUploadButtonMessage = function(view, inProgress) {
|
||||
var iconName = inProgress ? 'fa-spinner' : 'fa-camera';
|
||||
var message = inProgress ? view.titleUploading : view.uploadButtonTitle();
|
||||
expect(view.$('.upload-button-icon span').attr('class')).toContain(iconName);
|
||||
expect(view.$('.upload-button-title').text().trim()).toBe(message);
|
||||
};
|
||||
|
||||
var verifyImageRemoveButtonMessage = function(view, inProgress) {
|
||||
var iconName = inProgress ? 'fa-spinner' : 'fa-remove';
|
||||
var message = inProgress ? view.titleRemoving : view.removeButtonTitle();
|
||||
expect(view.$('.remove-button-icon span').attr('class')).toContain(iconName);
|
||||
expect(view.$('.remove-button-title').text().trim()).toBe(message);
|
||||
};
|
||||
|
||||
it('can upload profile image', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var imageName = 'profile_image.jpg';
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
var data;
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
// For default image, image title should be `Upload an image`
|
||||
verifyImageUploadButtonMessage(imageView, false);
|
||||
|
||||
// Add image to upload queue. Validate the image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
|
||||
|
||||
// Send 204 NO CONTENT to confirm the image upload success
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
// Upon successful image upload, account settings model will be fetched to
|
||||
// get the url for newly uploaded image, So we need to send the response for that GET
|
||||
data = {
|
||||
profile_image: {
|
||||
image_url_large: '/media/profile-images/' + imageName,
|
||||
has_image: true
|
||||
}
|
||||
};
|
||||
AjaxHelpers.respondWithJson(requests, data);
|
||||
|
||||
// Verify uploaded image name
|
||||
expect(imageView.$('.image-frame').attr('src')).toContain(imageName);
|
||||
|
||||
// Remove button should be present after successful image upload
|
||||
expect(imageView.$('.u-field-remove-button').css('display') !== 'none').toBeTruthy();
|
||||
|
||||
// After image upload, image title should be `Change image`
|
||||
verifyImageUploadButtonMessage(imageView, false);
|
||||
});
|
||||
|
||||
it('can remove profile image', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
var data;
|
||||
imageView.render();
|
||||
|
||||
// Verify image remove title
|
||||
verifyImageRemoveButtonMessage(imageView, false);
|
||||
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
// Verify image remove progress message
|
||||
verifyImageRemoveButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image remove
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_REMOVE_API_URL, null);
|
||||
|
||||
// Send 204 NO CONTENT to confirm the image removal success
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
|
||||
// Upon successful image removal, account settings model will be fetched to get default image url
|
||||
// So we need to send the response for that GET
|
||||
data = {
|
||||
profile_image: {
|
||||
image_url_large: '/media/profile-images/default.jpg',
|
||||
has_image: false
|
||||
}
|
||||
};
|
||||
AjaxHelpers.respondWithJson(requests, data);
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
});
|
||||
|
||||
it("can't remove default profile image", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
// Remove button should not be present for default image
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
// Remove button click handler should not be called
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can't upload image having size greater than max size", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate the image size
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(70)]});
|
||||
|
||||
// Verify error message
|
||||
expect($('.message-banner').text().trim())
|
||||
.toBe('The file must be smaller than 64 bytes in size.');
|
||||
});
|
||||
|
||||
it("can't upload image having size less than min size", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate the image size
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(10)]});
|
||||
|
||||
// Verify error message
|
||||
expect($('.message-banner').text().trim()).toBe('The file must be at least 16 bytes in size.');
|
||||
});
|
||||
|
||||
it("can't upload and remove image if parental consent required", function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false, yearOfBirth: ''});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedUploadButton');
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-upload-button').click();
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can't upload and remove image on others profile", function() {
|
||||
var imageView = createImageView({ownProfile: false});
|
||||
imageView.render();
|
||||
|
||||
spyOn(imageView, 'clickedUploadButton');
|
||||
spyOn(imageView, 'clickedRemoveButton');
|
||||
|
||||
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
|
||||
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
|
||||
|
||||
imageView.$('.u-field-upload-button').click();
|
||||
imageView.$('.u-field-remove-button').click();
|
||||
|
||||
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
|
||||
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows message if we try to navigate away during image upload/remove', function() {
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
spyOn(imageView, 'onBeforeUnload');
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue, this will validate image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
window.onbeforeunload = null;
|
||||
$(window).trigger('beforeunload');
|
||||
expect(imageView.onBeforeUnload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows error message for HTTP 500', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var imageView = createImageView({ownProfile: true, hasImage: false});
|
||||
imageView.render();
|
||||
|
||||
initializeUploader(imageView);
|
||||
|
||||
// Add image to upload queue. Validate the image size and send POST request to upload image
|
||||
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
// Verify image upload progress message
|
||||
verifyImageUploadButtonMessage(imageView, true);
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
|
||||
|
||||
// Send HTTP 500
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
/* eslint-disable vars-on-top */
|
||||
define(
|
||||
[
|
||||
'gettext',
|
||||
'backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'edx-ui-toolkit/js/pagination/paging-collection',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'js/spec/student_account/helpers',
|
||||
'learner_profile/js/spec_helpers/helpers',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model',
|
||||
'js/student_account/models/user_preferences_model',
|
||||
'learner_profile/js/views/learner_profile_fields',
|
||||
'learner_profile/js/views/learner_profile_view',
|
||||
'js/student_account/views/account_settings_fields',
|
||||
'js/views/message_banner'
|
||||
],
|
||||
function(gettext, Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers,
|
||||
FieldViews, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
|
||||
AccountSettingsFieldViews, MessageBannerView) {
|
||||
'use strict';
|
||||
|
||||
describe('edx.user.LearnerProfileView', function() {
|
||||
var createLearnerProfileView = function(ownProfile, accountPrivacy, profileIsPublic) {
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set(Helpers.createAccountSettingsData());
|
||||
accountSettingsModel.set({profile_is_public: profileIsPublic});
|
||||
accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE});
|
||||
|
||||
var accountPreferencesModel = new AccountPreferencesModel();
|
||||
accountPreferencesModel.set({account_privacy: accountPrivacy});
|
||||
|
||||
accountPreferencesModel.url = Helpers.USER_PREFERENCES_API_URL;
|
||||
|
||||
var editable = ownProfile ? 'toggle' : 'never';
|
||||
|
||||
var accountPrivacyFieldView = new LearnerProfileFields.AccountPrivacyFieldView({
|
||||
model: accountPreferencesModel,
|
||||
required: true,
|
||||
editable: 'always',
|
||||
showMessages: false,
|
||||
title: 'edX learners can see my:',
|
||||
valueAttribute: 'account_privacy',
|
||||
options: [
|
||||
['all_users', 'Full Profile'],
|
||||
['private', 'Limited Profile']
|
||||
],
|
||||
helpMessage: '',
|
||||
accountSettingsPageUrl: '/account/settings/'
|
||||
});
|
||||
|
||||
var messageView = new MessageBannerView({
|
||||
el: $('.message-banner')
|
||||
});
|
||||
|
||||
var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'profile_image',
|
||||
editable: editable,
|
||||
messageView: messageView,
|
||||
imageMaxBytes: Helpers.IMAGE_MAX_BYTES,
|
||||
imageMinBytes: Helpers.IMAGE_MIN_BYTES,
|
||||
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
|
||||
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
|
||||
});
|
||||
|
||||
var usernameFieldView = new FieldViews.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'username',
|
||||
helpMessage: ''
|
||||
});
|
||||
|
||||
var nameFieldView = new FieldViews.ReadonlyFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'name',
|
||||
helpMessage: ''
|
||||
});
|
||||
|
||||
var sectionOneFieldViews = [
|
||||
new LearnerProfileFields.SocialLinkIconsView({
|
||||
model: accountSettingsModel,
|
||||
socialPlatforms: Helpers.SOCIAL_PLATFORMS,
|
||||
ownProfile: true
|
||||
}),
|
||||
|
||||
new FieldViews.DropdownFieldView({
|
||||
title: gettext('Location'),
|
||||
model: accountSettingsModel,
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
placeholderValue: '',
|
||||
valueAttribute: 'country',
|
||||
options: Helpers.FIELD_OPTIONS,
|
||||
helpMessage: ''
|
||||
}),
|
||||
|
||||
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
|
||||
title: gettext('Language'),
|
||||
model: accountSettingsModel,
|
||||
required: false,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
placeholderValue: 'Add language',
|
||||
valueAttribute: 'language_proficiencies',
|
||||
options: Helpers.FIELD_OPTIONS,
|
||||
helpMessage: ''
|
||||
}),
|
||||
|
||||
new FieldViews.DateFieldView({
|
||||
model: accountSettingsModel,
|
||||
valueAttribute: 'date_joined',
|
||||
helpMessage: ''
|
||||
})
|
||||
];
|
||||
|
||||
var sectionTwoFieldViews = [
|
||||
new FieldViews.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: 'About me',
|
||||
placeholderValue: 'Tell other edX learners a little about yourself: where you live, '
|
||||
+ "what your interests are, why you're taking courses on edX, or what you hope to learn.",
|
||||
valueAttribute: 'bio',
|
||||
helpMessage: '',
|
||||
messagePosition: 'header'
|
||||
})
|
||||
];
|
||||
|
||||
return new LearnerProfileView(
|
||||
{
|
||||
el: $('.wrapper-profile'),
|
||||
ownProfile: ownProfile,
|
||||
hasPreferencesAccess: true,
|
||||
accountSettingsModel: accountSettingsModel,
|
||||
preferencesModel: accountPreferencesModel,
|
||||
accountPrivacyFieldView: accountPrivacyFieldView,
|
||||
usernameFieldView: usernameFieldView,
|
||||
nameFieldView: nameFieldView,
|
||||
profileImageFieldView: profileImageFieldView,
|
||||
sectionOneFieldViews: sectionOneFieldViews,
|
||||
sectionTwoFieldViews: sectionTwoFieldViews,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('learner_profile/fixtures/learner_profile.html');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
it('shows loading error correctly', function() {
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users');
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
learnerProfileView.showLoadingError();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
|
||||
});
|
||||
|
||||
it('renders all fields as expected for self with full access', function() {
|
||||
var learnerProfileView = createLearnerProfileView(true, 'all_users', true);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it('renders all fields as expected for self with limited access', function() {
|
||||
var learnerProfileView = createLearnerProfileView(true, 'private', false);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
|
||||
});
|
||||
|
||||
it('renders the fields as expected for others with full access', function() {
|
||||
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
|
||||
});
|
||||
|
||||
it('renders the fields as expected for others with limited access', function() {
|
||||
var learnerProfileView = createLearnerProfileView(false, 'private', false);
|
||||
|
||||
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
|
||||
learnerProfileView.render();
|
||||
|
||||
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
|
||||
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
/* eslint-disable vars-on-top */
|
||||
define(
|
||||
[
|
||||
'backbone', 'jquery', 'underscore',
|
||||
'js/spec/student_account/helpers',
|
||||
'learner_profile/js/views/section_two_tab',
|
||||
'js/views/fields',
|
||||
'js/student_account/models/user_account_model'
|
||||
],
|
||||
function(Backbone, $, _, Helpers, SectionTwoTabView, FieldViews, UserAccountModel) {
|
||||
'use strict';
|
||||
|
||||
describe('edx.user.SectionTwoTab', function() {
|
||||
var createSectionTwoView = function(ownProfile, profileIsPublic) {
|
||||
var accountSettingsModel = new UserAccountModel();
|
||||
accountSettingsModel.set(Helpers.createAccountSettingsData());
|
||||
accountSettingsModel.set({profile_is_public: profileIsPublic});
|
||||
accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE});
|
||||
|
||||
var editable = ownProfile ? 'toggle' : 'never';
|
||||
|
||||
var sectionTwoFieldViews = [
|
||||
new FieldViews.TextareaFieldView({
|
||||
model: accountSettingsModel,
|
||||
editable: editable,
|
||||
showMessages: false,
|
||||
title: 'About me',
|
||||
placeholderValue: 'Tell other edX learners a little about yourself: where you live, '
|
||||
+ "what your interests are, why you're taking courses on edX, or what you hope to learn.",
|
||||
valueAttribute: 'bio',
|
||||
helpMessage: '',
|
||||
messagePosition: 'header'
|
||||
})
|
||||
];
|
||||
|
||||
return new SectionTwoTabView({
|
||||
viewList: sectionTwoFieldViews,
|
||||
showFullProfile: function() {
|
||||
return profileIsPublic;
|
||||
},
|
||||
ownProfile: ownProfile
|
||||
});
|
||||
};
|
||||
|
||||
it('full profile displayed for public profile', function() {
|
||||
var view = createSectionTwoView(false, true);
|
||||
view.render();
|
||||
var bio = view.$el.find('.u-field-bio');
|
||||
expect(bio.length).toBe(1);
|
||||
});
|
||||
|
||||
it('profile field parts are actually rendered for public profile', function() {
|
||||
var view = createSectionTwoView(false, true);
|
||||
_.each(view.options.viewList, function(fieldView) {
|
||||
spyOn(fieldView, 'render').and.callThrough();
|
||||
});
|
||||
view.render();
|
||||
_.each(view.options.viewList, function(fieldView) {
|
||||
expect(fieldView.render).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
var testPrivateProfile = function(ownProfile, messageString) {
|
||||
var view = createSectionTwoView(ownProfile, false);
|
||||
view.render();
|
||||
var bio = view.$el.find('.u-field-bio');
|
||||
expect(bio.length).toBe(0);
|
||||
var msg = view.$el.find('span.profile-private-message');
|
||||
expect(msg.length).toBe(1);
|
||||
expect(_.count(msg.html(), messageString)).toBeTruthy();
|
||||
};
|
||||
|
||||
it('no profile when profile is private for other people', function() {
|
||||
testPrivateProfile(false, 'This learner is currently sharing a limited profile');
|
||||
});
|
||||
|
||||
it('no profile when profile is private for the user herself', function() {
|
||||
testPrivateProfile(true, 'You are currently sharing a limited profile');
|
||||
});
|
||||
|
||||
var testProfilePrivatePartsDoNotRender = function(ownProfile) {
|
||||
var view = createSectionTwoView(ownProfile, false);
|
||||
_.each(view.options.viewList, function(fieldView) {
|
||||
spyOn(fieldView, 'render');
|
||||
});
|
||||
view.render();
|
||||
_.each(view.options.viewList, function(fieldView) {
|
||||
expect(fieldView.render).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
it('profile field parts are not rendered for private profile for owner', function() {
|
||||
testProfilePrivatePartsDoNotRender(true);
|
||||
});
|
||||
|
||||
it('profile field parts are not rendered for private profile for other people', function() {
|
||||
testProfilePrivatePartsDoNotRender(false);
|
||||
});
|
||||
|
||||
it('does not allow fields to be edited when visiting a profile for other people', function() {
|
||||
var view = createSectionTwoView(false, true);
|
||||
var bio = view.options.viewList[0];
|
||||
expect(bio.editable).toBe('never');
|
||||
});
|
||||
|
||||
it("allows fields to be edited when visiting one's own profile", function() {
|
||||
var view = createSectionTwoView(true, true);
|
||||
var bio = view.options.viewList[0];
|
||||
expect(bio.editable).toBe('toggle');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,133 +0,0 @@
|
||||
define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'], function(_, URI, AjaxHelpers) {
|
||||
'use strict';
|
||||
|
||||
var expectProfileElementContainsField = function(element, view) {
|
||||
var titleElement, fieldTitle;
|
||||
var $element = $(element);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if ('fieldValue' in view || 'imageUrl' in view) {
|
||||
if ('imageUrl' in view) {
|
||||
expect($($element.find('.image-frame')[0]).attr('src')).toBe(view.imageUrl());
|
||||
} else if (view.fieldType === 'date') {
|
||||
expect(view.fieldValue()).toBe(view.timezoneFormattedDate());
|
||||
} else if (view.fieldValue()) {
|
||||
expect(view.fieldValue()).toBe(view.modelValue());
|
||||
} else if ('optionForValue' in view) {
|
||||
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(
|
||||
view.displayValue(view.modelValue())
|
||||
);
|
||||
} else {
|
||||
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(view.modelValue());
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected field type: ' + view.fieldType);
|
||||
}
|
||||
};
|
||||
|
||||
var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) {
|
||||
var $accountPrivacyElement = $('.wrapper-profile-field-account-privacy');
|
||||
var $privacyFieldElement = $($accountPrivacyElement).find('.u-field');
|
||||
|
||||
if (othersProfile) {
|
||||
expect($privacyFieldElement.length).toBe(0);
|
||||
} else {
|
||||
expect($privacyFieldElement.length).toBe(1);
|
||||
expectProfileElementContainsField($privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView);
|
||||
}
|
||||
};
|
||||
|
||||
var expectSectionOneTobeRendered = function(learnerProfileView) {
|
||||
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one'))
|
||||
.find('.u-field, .social-links');
|
||||
|
||||
expect(sectionOneFieldElements.length).toBe(7);
|
||||
expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView);
|
||||
expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView);
|
||||
expectProfileElementContainsField(sectionOneFieldElements[2], learnerProfileView.options.nameFieldView);
|
||||
|
||||
_.each(_.rest(sectionOneFieldElements, 3), function(sectionFieldElement, fieldIndex) {
|
||||
expectProfileElementContainsField(
|
||||
sectionFieldElement,
|
||||
learnerProfileView.options.sectionOneFieldViews[fieldIndex]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var expectSectionTwoTobeRendered = function(learnerProfileView) {
|
||||
var $sectionTwoElement = $('.wrapper-profile-section-two');
|
||||
var $sectionTwoFieldElements = $($sectionTwoElement).find('.u-field');
|
||||
|
||||
expect($sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length);
|
||||
|
||||
_.each($sectionTwoFieldElements, function(sectionFieldElement, fieldIndex) {
|
||||
expectProfileElementContainsField(
|
||||
sectionFieldElement,
|
||||
learnerProfileView.options.sectionTwoFieldViews[fieldIndex]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var expectProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) {
|
||||
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
|
||||
expectSectionOneTobeRendered(learnerProfileView);
|
||||
expectSectionTwoTobeRendered(learnerProfileView);
|
||||
};
|
||||
|
||||
var expectLimitedProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) {
|
||||
var sectionOneFieldElements = $('.wrapper-profile-section-one').find('.u-field');
|
||||
|
||||
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
|
||||
|
||||
expect(sectionOneFieldElements.length).toBe(2);
|
||||
expectProfileElementContainsField(
|
||||
sectionOneFieldElements[0],
|
||||
learnerProfileView.options.profileImageFieldView
|
||||
);
|
||||
expectProfileElementContainsField(
|
||||
sectionOneFieldElements[1],
|
||||
learnerProfileView.options.usernameFieldView
|
||||
);
|
||||
|
||||
if (othersProfile) {
|
||||
expect($('.profile-private-message').text())
|
||||
.toBe('This learner is currently sharing a limited profile.');
|
||||
} else {
|
||||
expect($('.profile-private-message').text()).toBe('You are currently sharing a limited profile.');
|
||||
}
|
||||
};
|
||||
|
||||
var expectProfileSectionsNotToBeRendered = function() {
|
||||
expect($('.wrapper-profile-field-account-privacy').length).toBe(0);
|
||||
expect($('.wrapper-profile-section-one').length).toBe(0);
|
||||
expect($('.wrapper-profile-section-two').length).toBe(0);
|
||||
};
|
||||
|
||||
var expectTabbedViewToBeUndefined = function(requests, tabbedViewView) {
|
||||
// Unrelated initial request, no badge request
|
||||
expect(requests.length).toBe(1);
|
||||
expect(tabbedViewView).toBe(undefined);
|
||||
};
|
||||
|
||||
var expectTabbedViewToBeShown = function(tabbedViewView) {
|
||||
expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(true);
|
||||
};
|
||||
|
||||
return {
|
||||
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
|
||||
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered,
|
||||
expectTabbedViewToBeUndefined: expectTabbedViewToBeUndefined,
|
||||
expectTabbedViewToBeShown: expectTabbedViewToBeShown
|
||||
};
|
||||
});
|
||||
@@ -1,169 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
(function(define) {
|
||||
'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',
|
||||
'text!learner_profile/templates/social_icons.underscore',
|
||||
'backbone-super'
|
||||
], function(gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView, socialIconsTemplate) {
|
||||
var LearnerProfileFieldViews = {};
|
||||
|
||||
LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({
|
||||
|
||||
events: {
|
||||
'click button.btn-change-privacy': 'finishEditing',
|
||||
'change select': 'showSaveButton'
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this._super();
|
||||
this.showNotificationMessage();
|
||||
this.updateFieldValue();
|
||||
return this;
|
||||
},
|
||||
|
||||
showNotificationMessage: function() {
|
||||
var accountSettingsLink = HtmlUtils.joinHtml(
|
||||
HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML('<a href="{settings_url}">'), {settings_url: this.options.accountSettingsPageUrl}
|
||||
),
|
||||
gettext('Account Settings page.'),
|
||||
HtmlUtils.HTML('</a>')
|
||||
);
|
||||
if (this.profileIsPrivate) {
|
||||
this._super(
|
||||
HtmlUtils.interpolateHtml(
|
||||
gettext('You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}'), // eslint-disable-line max-len
|
||||
{account_settings_page_link: accountSettingsLink}
|
||||
)
|
||||
);
|
||||
} else if (this.requiresParentalConsent) {
|
||||
this._super(
|
||||
HtmlUtils.interpolateHtml(
|
||||
gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'), // eslint-disable-line max-len
|
||||
{account_settings_page_link: accountSettingsLink}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this._super('');
|
||||
}
|
||||
},
|
||||
|
||||
updateFieldValue: function() {
|
||||
if (!this.isAboveMinimumAge) {
|
||||
this.$('.u-field-value select').val('private');
|
||||
this.disableField(true);
|
||||
}
|
||||
},
|
||||
|
||||
showSaveButton: function() {
|
||||
$('.btn-change-privacy').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
LearnerProfileFieldViews.ProfileImageFieldView = ImageFieldView.extend({
|
||||
|
||||
screenReaderTitle: gettext('Profile Image'),
|
||||
|
||||
imageUrl: function() {
|
||||
return this.model.profileImageUrl();
|
||||
},
|
||||
|
||||
imageAltText: function() {
|
||||
return StringUtils.interpolate(
|
||||
gettext('Profile image for {username}'),
|
||||
{username: this.model.get('username')}
|
||||
);
|
||||
},
|
||||
|
||||
imageChangeSucceeded: function() {
|
||||
var view = this;
|
||||
// Update model to get the latest urls of profile image.
|
||||
this.model.fetch().done(function() {
|
||||
view.setCurrentStatus('');
|
||||
view.render();
|
||||
view.$('.u-field-upload-button').focus();
|
||||
}).fail(function() {
|
||||
view.setCurrentStatus('');
|
||||
view.showErrorMessage(view.errorMessage);
|
||||
});
|
||||
},
|
||||
|
||||
imageChangeFailed: function(e, data) {
|
||||
this.setCurrentStatus('');
|
||||
this.showImageChangeFailedMessage(data.jqXHR.status, data.jqXHR.responseText);
|
||||
},
|
||||
|
||||
showImageChangeFailedMessage: function(status, responseText) {
|
||||
var errors;
|
||||
if (_.contains([400, 404], status)) {
|
||||
try {
|
||||
errors = JSON.parse(responseText);
|
||||
this.showErrorMessage(errors.user_message);
|
||||
} catch (error) {
|
||||
this.showErrorMessage(this.errorMessage);
|
||||
}
|
||||
} else {
|
||||
this.showErrorMessage(this.errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
showErrorMessage: function(message) {
|
||||
this.options.messageView.showMessage(message);
|
||||
},
|
||||
|
||||
isEditingAllowed: function() {
|
||||
return this.model.isAboveMinimumAge();
|
||||
},
|
||||
|
||||
isShowingPlaceholder: function() {
|
||||
return !this.model.hasProfileImage();
|
||||
},
|
||||
|
||||
clickedRemoveButton: function(e, data) {
|
||||
this.options.messageView.hideMessage();
|
||||
this._super(e, data);
|
||||
},
|
||||
|
||||
fileSelected: function(e, data) {
|
||||
this.options.messageView.hideMessage();
|
||||
this._super(e, data);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -1,150 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
[
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'common/js/components/views/tabbed_view',
|
||||
'learner_profile/js/views/section_two_tab'
|
||||
],
|
||||
function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab) {
|
||||
var LearnerProfileView = Backbone.View.extend({
|
||||
|
||||
initialize: function(options) {
|
||||
var Router;
|
||||
this.options = _.extend({}, options);
|
||||
_.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError');
|
||||
this.listenTo(this.options.preferencesModel, 'change:account_privacy', this.render);
|
||||
Router = Backbone.Router.extend({
|
||||
routes: {':about_me': 'loadTab', ':accomplishments': 'loadTab'}
|
||||
});
|
||||
|
||||
this.router = new Router();
|
||||
this.firstRender = true;
|
||||
},
|
||||
|
||||
showFullProfile: function() {
|
||||
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
|
||||
if (this.options.ownProfile) {
|
||||
return isAboveMinimumAge
|
||||
&& this.options.preferencesModel.get('account_privacy') === 'all_users';
|
||||
} else {
|
||||
return this.options.accountSettingsModel.get('profile_is_public');
|
||||
}
|
||||
},
|
||||
|
||||
setActiveTab: function(tab) {
|
||||
// This tab may not actually exist.
|
||||
if (this.tabbedView.getTabMeta(tab).tab) {
|
||||
this.tabbedView.setActiveTab(tab);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tabs,
|
||||
$tabbedViewElement,
|
||||
$wrapperProfileBioElement = this.$el.find('.wrapper-profile-bio'),
|
||||
self = this;
|
||||
|
||||
this.sectionTwoView = new SectionTwoTab({
|
||||
viewList: this.options.sectionTwoFieldViews,
|
||||
showFullProfile: this.showFullProfile,
|
||||
ownProfile: this.options.ownProfile
|
||||
});
|
||||
|
||||
this.renderFields();
|
||||
|
||||
// Reveal the profile and hide the loading indicator
|
||||
$('.ui-loading-indicator').addClass('is-hidden');
|
||||
$('.wrapper-profile-section-container-one').removeClass('is-hidden');
|
||||
$('.wrapper-profile-section-container-two').removeClass('is-hidden');
|
||||
|
||||
|
||||
if (this.showFullProfile()) {
|
||||
tabs = [
|
||||
{view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'}
|
||||
];
|
||||
|
||||
this.tabbedView = new TabbedView({
|
||||
tabs: tabs,
|
||||
router: this.router,
|
||||
viewLabel: gettext('Profile')
|
||||
});
|
||||
|
||||
$tabbedViewElement = this.tabbedView.render().el;
|
||||
HtmlUtils.setHtml(
|
||||
$wrapperProfileBioElement,
|
||||
HtmlUtils.HTML($tabbedViewElement)
|
||||
);
|
||||
|
||||
if (this.firstRender) {
|
||||
this.router.on('route:loadTab', _.bind(this.setActiveTab, this));
|
||||
Backbone.history.start();
|
||||
this.firstRender = false;
|
||||
// Load from history.
|
||||
this.router.navigate((Backbone.history.getFragment() || 'about_me'), {trigger: true});
|
||||
} else {
|
||||
// Restart the router so the tab will be brought up anew.
|
||||
Backbone.history.stop();
|
||||
Backbone.history.start();
|
||||
}
|
||||
} else {
|
||||
if (this.isCoppaCompliant()) {
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
$wrapperProfileBioElement.html(this.sectionTwoView.render().el);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
isCoppaCompliant: function() {
|
||||
var enableCoppaCompliance = this.options.accountSettingsModel.get('enable_coppa_compliance'),
|
||||
isAboveAge = this.options.accountSettingsModel.isAboveMinimumAge();
|
||||
return !enableCoppaCompliance || (enableCoppaCompliance && isAboveAge);
|
||||
},
|
||||
|
||||
renderFields: function() {
|
||||
var view = this,
|
||||
fieldView,
|
||||
imageView,
|
||||
settings;
|
||||
|
||||
if (this.options.ownProfile && this.isCoppaCompliant()) {
|
||||
fieldView = this.options.accountPrivacyFieldView;
|
||||
settings = this.options.accountSettingsModel;
|
||||
fieldView.profileIsPrivate = !settings.get('year_of_birth');
|
||||
fieldView.requiresParentalConsent = settings.get('requires_parental_consent');
|
||||
fieldView.isAboveMinimumAge = settings.isAboveMinimumAge();
|
||||
fieldView.undelegateEvents();
|
||||
this.$('.wrapper-profile-field-account-privacy').prepend(fieldView.render().el);
|
||||
fieldView.delegateEvents();
|
||||
}
|
||||
|
||||
// Clear existing content in user profile card
|
||||
this.$('.profile-section-one-fields').html('');
|
||||
|
||||
// Do not show name when in limited mode or no name has been set
|
||||
if (this.showFullProfile() && this.options.accountSettingsModel.get('name')) {
|
||||
this.$('.profile-section-one-fields').append(this.options.nameFieldView.render().el);
|
||||
}
|
||||
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
|
||||
|
||||
imageView = this.options.profileImageFieldView;
|
||||
this.$('.profile-image-field').append(imageView.render().el);
|
||||
|
||||
if (this.showFullProfile()) {
|
||||
_.each(this.options.sectionOneFieldViews, function(childFieldView) {
|
||||
view.$('.profile-section-one-fields').append(childFieldView.render().el);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showLoadingError: function() {
|
||||
this.$('.ui-loading-indicator').addClass('is-hidden');
|
||||
this.$('.ui-loading-error').removeClass('is-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
return LearnerProfileView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -1,33 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
[
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'text!learner_profile/templates/section_two.underscore',
|
||||
'edx-ui-toolkit/js/utils/html-utils'
|
||||
],
|
||||
function(gettext, $, _, Backbone, sectionTwoTemplate, HtmlUtils) {
|
||||
var SectionTwoTab = Backbone.View.extend({
|
||||
attributes: {
|
||||
class: 'wrapper-profile-section-two'
|
||||
},
|
||||
template: _.template(sectionTwoTemplate),
|
||||
initialize: function(options) {
|
||||
this.options = _.extend({}, options);
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var showFullProfile = this.options.showFullProfile();
|
||||
this.$el.html(HtmlUtils.HTML(this.template({ownProfile: self.options.ownProfile, showFullProfile: showFullProfile})).toString()); // eslint-disable-line max-len
|
||||
if (showFullProfile) {
|
||||
_.each(this.options.viewList, function(fieldView) {
|
||||
self.$el.find('.field-container').append(fieldView.render().el);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return SectionTwoTab;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="profile-section-two-fields">
|
||||
<div class="field-container"></div>
|
||||
<% if (!showFullProfile) { %>
|
||||
<% if(ownProfile) { %>
|
||||
<span class="profile-private-message"><%- gettext("You are currently sharing a limited profile.") %></span>
|
||||
<% } else { %>
|
||||
<span class="profile-private-message"><%- gettext("This learner is currently sharing a limited profile.") %></span>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="social-links">
|
||||
<% for (var platform in socialLinks) { %>
|
||||
<% if (socialLinks[platform]) { %>
|
||||
<a rel="noopener" target="_blank" href= <%-socialLinks[platform]%>>
|
||||
<span class="icon fa fa-<%-platform%>-square" data-platform=<%-platform%> aria-hidden="true"></span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.utils.translation import gettext as _
|
||||
from common.djangoapps.third_party_auth import pipeline
|
||||
%>
|
||||
|
||||
<li class="controls--account">
|
||||
<span class="title">
|
||||
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
|
||||
${_("Connected Accounts")}
|
||||
</span>
|
||||
<span class="data">
|
||||
<span class="third-party-auth">
|
||||
% for state in provider_user_states:
|
||||
<div class="auth-provider">
|
||||
<div class="status">
|
||||
% if state.has_account:
|
||||
<span class="icon fa fa-link" aria-hidden="true"></span> <span class="copy">${_('Linked')}</span>
|
||||
% else:
|
||||
<span class="icon fa fa-unlink" aria-hidden="true"></span><span class="copy">${_('Not Linked')}</span>
|
||||
% endif
|
||||
</div>
|
||||
<span class="provider">${state.provider.name}</span>
|
||||
<span class="control">
|
||||
<form
|
||||
action="${pipeline.get_disconnect_url(state.provider.provider_id, state.association_id)}"
|
||||
method="post"
|
||||
name="${state.get_unlink_form_name()}">
|
||||
% if state.has_account:
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
|
||||
<button type="button" onclick="document.${state.get_unlink_form_name()}.submit()">
|
||||
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Unlink")}
|
||||
</button>
|
||||
% elif state.provider.display_for_login:
|
||||
<a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}">
|
||||
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
|
||||
${_("Link")}
|
||||
</a>
|
||||
% endif
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
% endfor
|
||||
</span>
|
||||
</li>
|
||||
@@ -1,69 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%!
|
||||
from django.utils.translation import gettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<div class="learner-achievements">
|
||||
% if course_certificates or own_profile:
|
||||
<h3 class="u-field-title">Course Certificates</h3>
|
||||
% if course_certificates:
|
||||
% for certificate in course_certificates:
|
||||
<%
|
||||
course = certificate['course']
|
||||
|
||||
completion_date_message_html = Text(_('Completed {completion_date_html}')).format(
|
||||
completion_date_html=HTML(
|
||||
'<span'
|
||||
' class="localized-datetime start-date"'
|
||||
' data-datetime="{completion_date}"'
|
||||
' data-format="shortDate"'
|
||||
' data-timezone="{user_timezone}"'
|
||||
' data-language="{user_language}"'
|
||||
'></span>'
|
||||
).format(
|
||||
completion_date=certificate['created'],
|
||||
user_timezone=user_timezone,
|
||||
user_language=user_language,
|
||||
),
|
||||
)
|
||||
%>
|
||||
<div class="card certificate-card mode-${certificate['type']}">
|
||||
<div class="card-logo">
|
||||
<h4 class="sr-only">
|
||||
${_('{course_mode} certificate').format(
|
||||
course_mode=certificate['type'],
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-supertitle">${course.display_org_with_default}</div>
|
||||
<div class="card-title">${course.display_name_with_default}</div>
|
||||
<p class="card-text">${completion_date_message_html}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
% elif own_profile:
|
||||
<div class="learner-message">
|
||||
<h4 class="message-header">${_("You haven't earned any certificates yet.")}</h4>
|
||||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
|
||||
<p class="message-actions">
|
||||
<a class="btn btn-brand" href="${marketing_link('COURSES')}">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
${_('Explore New Courses')}
|
||||
</a>
|
||||
</p>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
@@ -1,79 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
<%def name="online_help_token()"><% return "profile" %></%def>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%!
|
||||
import json
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("Learner Profile")}</%block>
|
||||
|
||||
<%block name="bodyclass">view-profile</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="wrapper-profile">
|
||||
<div class="profile ${'profile-self' if own_profile else 'profile-other'}">
|
||||
<div class="wrapper-profile-field-account-privacy">
|
||||
% if own_profile and records_url:
|
||||
<div class="wrapper-profile-records">
|
||||
<a href="${records_url}">
|
||||
<button class="btn profile-records-button">${_("View My Records")}</button>
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% if own_profile:
|
||||
<div class="profile-header">
|
||||
<h2 class="header">${_("My Profile")}</h2>
|
||||
<div class="subheader">
|
||||
${_('Build out your profile to personalize your identity on {platform_name}.').format(
|
||||
platform_name=platform_name,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class="wrapper-profile-sections account-settings-container">
|
||||
<div class="ui-loading-indicator">
|
||||
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-one is-hidden">
|
||||
<div class="wrapper-profile-section-one">
|
||||
<div class="profile-image-field">
|
||||
</div>
|
||||
<div class="profile-section-one-fields">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
<span class="copy">${_("An error occurred. Try loading the page again.")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-profile-section-container-two is-hidden">
|
||||
<div class="wrapper-profile-bio"></div>
|
||||
% if achievements_fragment:
|
||||
${HTML(achievements_fragment.body_html())}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory">
|
||||
var options = ${data | n, dump_js_escaped_json};
|
||||
LearnerProfileFactory(options);
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
@@ -1,281 +0,0 @@
|
||||
""" Tests for student profile views. """
|
||||
|
||||
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.util.testing import UrlResetMixin
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.features.learner_profile.toggles import REDIRECT_TO_PROFILE_MICROFRONTEND
|
||||
from openedx.features.learner_profile.views.learner_profile import learner_profile_context
|
||||
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LearnerProfileViewTest(SiteMixin, UrlResetMixin, ModuleStoreTestCase):
|
||||
""" Tests for the student profile view. """
|
||||
|
||||
USERNAME = "username"
|
||||
OTHER_USERNAME = "other_user"
|
||||
PASSWORD = "password"
|
||||
DOWNLOAD_URL = "http://www.example.com/certificate.pdf"
|
||||
CONTEXT_DATA = [
|
||||
'default_public_account_fields',
|
||||
'accounts_api_url',
|
||||
'preferences_api_url',
|
||||
'account_settings_page_url',
|
||||
'has_preferences_access',
|
||||
'own_profile',
|
||||
'country_options',
|
||||
'language_options',
|
||||
'account_settings_data',
|
||||
'preferences_data',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.other_user = UserFactory.create(username=self.OTHER_USERNAME, password=self.PASSWORD)
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.course = CourseFactory.create(
|
||||
start=datetime.datetime(2013, 9, 16, 7, 17, 28),
|
||||
end=datetime.datetime.now(),
|
||||
certificate_available_date=datetime.datetime.now(),
|
||||
)
|
||||
|
||||
def test_context(self):
|
||||
"""
|
||||
Verify learner profile page context data.
|
||||
"""
|
||||
request = RequestFactory().get('/url')
|
||||
request.user = self.user
|
||||
|
||||
context = learner_profile_context(request, self.USERNAME, self.user.is_staff)
|
||||
|
||||
assert context['data']['default_public_account_fields'] == \
|
||||
settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields']
|
||||
|
||||
assert context['data']['accounts_api_url'] == \
|
||||
reverse('accounts_api', kwargs={'username': self.user.username})
|
||||
|
||||
assert context['data']['preferences_api_url'] == \
|
||||
reverse('preferences_api', kwargs={'username': self.user.username})
|
||||
|
||||
assert context['data']['profile_image_upload_url'] == \
|
||||
reverse('profile_image_upload', kwargs={'username': self.user.username})
|
||||
|
||||
assert context['data']['profile_image_remove_url'] == \
|
||||
reverse('profile_image_remove', kwargs={'username': self.user.username})
|
||||
|
||||
assert context['data']['profile_image_max_bytes'] == settings.PROFILE_IMAGE_MAX_BYTES
|
||||
|
||||
assert context['data']['profile_image_min_bytes'] == settings.PROFILE_IMAGE_MIN_BYTES
|
||||
|
||||
assert context['data']['account_settings_page_url'] == reverse('account_settings')
|
||||
|
||||
for attribute in self.CONTEXT_DATA:
|
||||
assert attribute in context['data']
|
||||
|
||||
def test_view(self):
|
||||
"""
|
||||
Verify learner profile page view.
|
||||
"""
|
||||
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
|
||||
response = self.client.get(path=profile_path)
|
||||
|
||||
for attribute in self.CONTEXT_DATA:
|
||||
self.assertContains(response, attribute)
|
||||
|
||||
def test_redirect_view(self):
|
||||
with override_waffle_flag(REDIRECT_TO_PROFILE_MICROFRONTEND, active=True):
|
||||
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
|
||||
|
||||
# Test with waffle flag active and site setting disabled, does not redirect
|
||||
response = self.client.get(path=profile_path)
|
||||
for attribute in self.CONTEXT_DATA:
|
||||
self.assertContains(response, attribute)
|
||||
|
||||
# Test with waffle flag active and site setting enabled, redirects to microfrontend
|
||||
site_domain = 'othersite.example.com'
|
||||
self.set_up_site(site_domain, {
|
||||
'SITE_NAME': site_domain,
|
||||
'ENABLE_PROFILE_MICROFRONTEND': True
|
||||
})
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
response = self.client.get(path=profile_path)
|
||||
profile_url = settings.PROFILE_MICROFRONTEND_URL
|
||||
self.assertRedirects(response, profile_url + self.USERNAME, fetch_redirect_response=False)
|
||||
|
||||
def test_records_link(self):
|
||||
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
|
||||
response = self.client.get(path=profile_path)
|
||||
self.assertContains(response, f'<a href="{CREDENTIALS_PUBLIC_SERVICE_URL}/records/">')
|
||||
|
||||
def test_undefined_profile_page(self):
|
||||
"""
|
||||
Verify that a 404 is returned for a non-existent profile page.
|
||||
"""
|
||||
profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"})
|
||||
response = self.client.get(path=profile_path)
|
||||
assert 404 == response.status_code
|
||||
|
||||
def _create_certificate(self, course_key=None, enrollment_mode=CourseMode.HONOR, status='downloadable'):
|
||||
"""Simulate that the user has a generated certificate. """
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, mode=enrollment_mode)
|
||||
return GeneratedCertificateFactory(
|
||||
user=self.user,
|
||||
course_id=course_key or self.course.id,
|
||||
mode=enrollment_mode,
|
||||
download_url=self.DOWNLOAD_URL,
|
||||
status=status,
|
||||
)
|
||||
|
||||
@ddt.data(CourseMode.HONOR, CourseMode.PROFESSIONAL, CourseMode.VERIFIED)
|
||||
def test_certificate_visibility(self, cert_mode):
|
||||
"""
|
||||
Verify that certificates are displayed with the correct card mode.
|
||||
"""
|
||||
# Add new certificate
|
||||
cert = self._create_certificate(enrollment_mode=cert_mode)
|
||||
cert.save()
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
|
||||
self.assertContains(response, f'card certificate-card mode-{cert_mode}')
|
||||
|
||||
@ddt.data(
|
||||
['downloadable', True],
|
||||
['notpassing', False],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_certificate_status_visibility(self, status, is_passed_status):
|
||||
"""
|
||||
Verify that certificates are only displayed for passing status.
|
||||
"""
|
||||
# Add new certificate
|
||||
cert = self._create_certificate(status=status)
|
||||
cert.save()
|
||||
|
||||
# Ensure that this test is actually using both passing and non-passing certs.
|
||||
assert CertificateStatuses.is_passing_status(cert.status) == is_passed_status
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
|
||||
if is_passed_status:
|
||||
self.assertContains(response, f'card certificate-card mode-{cert.mode}')
|
||||
else:
|
||||
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
|
||||
|
||||
def test_certificate_for_missing_course(self):
|
||||
"""
|
||||
Verify that a certificate is not shown for a missing course.
|
||||
"""
|
||||
# Add new certificate
|
||||
cert = self._create_certificate(course_key=CourseLocator.from_string('course-v1:edX+INVALID+1'))
|
||||
cert.save()
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
|
||||
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_no_certificate_visibility(self, own_profile):
|
||||
"""
|
||||
Verify that the 'You haven't earned any certificates yet.' well appears on the user's
|
||||
own profile when they do not have certificates and does not appear when viewing
|
||||
another user that does not have any certificates.
|
||||
"""
|
||||
profile_username = self.user.username if own_profile else self.other_user.username
|
||||
response = self.client.get(f'/u/{profile_username}')
|
||||
|
||||
if own_profile:
|
||||
self.assertContains(response, 'You haven't earned any certificates yet.')
|
||||
else:
|
||||
self.assertNotContains(response, 'You haven't earned any certificates yet.')
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_explore_courses_visibility(self, courses_browsable):
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'COURSES_ARE_BROWSABLE': courses_browsable}):
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
if courses_browsable:
|
||||
self.assertContains(response, 'Explore New Courses')
|
||||
else:
|
||||
self.assertNotContains(response, 'Explore New Courses')
|
||||
|
||||
def test_certificate_for_visibility_for_not_viewable_course(self):
|
||||
"""
|
||||
Verify that a certificate is not shown if certificate are not viewable to users.
|
||||
"""
|
||||
# add new course with certificate_available_date is future date.
|
||||
course = CourseFactory.create(
|
||||
certificate_available_date=datetime.datetime.now() + datetime.timedelta(days=5),
|
||||
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
)
|
||||
|
||||
cert = self._create_certificate(course_key=course.id)
|
||||
cert.save()
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
|
||||
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
|
||||
|
||||
def test_certificates_visible_only_for_staff_and_profile_user(self):
|
||||
"""
|
||||
Verify that certificates data are passed to template only in case of staff user
|
||||
and profile user.
|
||||
"""
|
||||
request = RequestFactory().get('/url')
|
||||
request.user = self.user
|
||||
profile_username = self.other_user.username
|
||||
user_is_staff = True
|
||||
context = learner_profile_context(request, profile_username, user_is_staff)
|
||||
|
||||
assert 'achievements_fragment' in context
|
||||
|
||||
user_is_staff = False
|
||||
context = learner_profile_context(request, profile_username, user_is_staff)
|
||||
assert 'achievements_fragment' not in context
|
||||
|
||||
profile_username = self.user.username
|
||||
context = learner_profile_context(request, profile_username, user_is_staff)
|
||||
assert 'achievements_fragment' in context
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
||||
def test_certificate_visibility_with_no_cert_config(self):
|
||||
"""
|
||||
Verify that certificates are not displayed until there is an active
|
||||
certificate configuration.
|
||||
"""
|
||||
# Add new certificate
|
||||
cert = self._create_certificate(enrollment_mode=CourseMode.VERIFIED)
|
||||
cert.download_url = ''
|
||||
cert.save()
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
self.assertNotContains(
|
||||
response, f'card certificate-card mode-{CourseMode.VERIFIED}'
|
||||
)
|
||||
|
||||
course_overview = CourseOverview.get_from_id(self.course.id)
|
||||
course_overview.has_any_active_web_certificate = True
|
||||
course_overview.save()
|
||||
|
||||
response = self.client.get(f'/u/{self.user.username}')
|
||||
self.assertContains(
|
||||
response, f'card certificate-card mode-{CourseMode.VERIFIED}'
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
Toggles for Learner Profile page.
|
||||
"""
|
||||
|
||||
|
||||
from edx_toggles.toggles import WaffleFlag
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
|
||||
# Namespace for learner profile waffle flags.
|
||||
WAFFLE_FLAG_NAMESPACE = 'learner_profile'
|
||||
|
||||
# Waffle flag to redirect to another learner profile experience.
|
||||
# .. toggle_name: learner_profile.redirect_to_microfrontend
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the profile page.
|
||||
# .. toggle_use_cases: temporary, open_edx
|
||||
# .. toggle_creation_date: 2019-02-19
|
||||
# .. toggle_target_removal_date: 2020-12-31
|
||||
# .. toggle_warning: Also set settings.PROFILE_MICROFRONTEND_URL and site's ENABLE_PROFILE_MICROFRONTEND.
|
||||
# .. toggle_tickets: DEPR-17
|
||||
REDIRECT_TO_PROFILE_MICROFRONTEND = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.redirect_to_microfrontend', __name__)
|
||||
|
||||
|
||||
def should_redirect_to_profile_microfrontend():
|
||||
return (
|
||||
configuration_helpers.get_value('ENABLE_PROFILE_MICROFRONTEND') and
|
||||
REDIRECT_TO_PROFILE_MICROFRONTEND.is_enabled()
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
Defines URLs for the learner profile.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import path, re_path
|
||||
|
||||
from openedx.features.learner_profile.views.learner_profile import learner_profile
|
||||
|
||||
from .views.learner_achievements import LearnerAchievementsFragmentView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r'^{username_pattern}$'.format(
|
||||
username_pattern=settings.USERNAME_PATTERN,
|
||||
),
|
||||
learner_profile,
|
||||
name='learner_profile',
|
||||
),
|
||||
path('achievements', LearnerAchievementsFragmentView.as_view(),
|
||||
name='openedx.learner_profile.learner_achievements_fragment_view',
|
||||
),
|
||||
]
|
||||
@@ -1,58 +0,0 @@
|
||||
"""
|
||||
Views to render a learner's achievements.
|
||||
"""
|
||||
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.certificates import api as certificate_api
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
|
||||
|
||||
class LearnerAchievementsFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment to render a learner's achievements.
|
||||
"""
|
||||
|
||||
def render_to_fragment(self, request, username=None, own_profile=False, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Renders the current learner's achievements.
|
||||
"""
|
||||
course_certificates = self._get_ordered_certificates_for_user(request, username)
|
||||
context = {
|
||||
'course_certificates': course_certificates,
|
||||
'own_profile': own_profile,
|
||||
'disable_courseware_js': True,
|
||||
}
|
||||
if course_certificates or own_profile:
|
||||
html = render_to_string('learner_profile/learner-achievements-fragment.html', context)
|
||||
return Fragment(html)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_ordered_certificates_for_user(self, request, username):
|
||||
"""
|
||||
Returns a user's certificates sorted by course name.
|
||||
"""
|
||||
course_certificates = certificate_api.get_certificates_for_user(username)
|
||||
passing_certificates = []
|
||||
for course_certificate in course_certificates:
|
||||
if course_certificate.get('is_passing', False):
|
||||
course_key = course_certificate['course_key']
|
||||
try:
|
||||
course_overview = CourseOverview.get_from_id(course_key)
|
||||
course_certificate['course'] = course_overview
|
||||
if certificate_api.certificates_viewable_for_course(course_overview):
|
||||
# add certificate into passing certificate list only if it's a PDF certificate
|
||||
# or there is an active certificate configuration.
|
||||
if course_certificate['is_pdf_certificate'] or course_overview.has_any_active_web_certificate:
|
||||
passing_certificates.append(course_certificate)
|
||||
except CourseOverview.DoesNotExist:
|
||||
# This is unlikely to fail as the course should exist.
|
||||
# Ideally the cert should have all the information that
|
||||
# it needs. This might be solved by the Credentials API.
|
||||
pass
|
||||
passing_certificates.sort(key=lambda certificate: certificate['course'].display_name_with_default)
|
||||
return passing_certificates
|
||||
@@ -1,128 +0,0 @@
|
||||
""" Views for a student's profile information. """
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django_countries import countries
|
||||
|
||||
from common.djangoapps.edxmako.shortcuts import marketing_link
|
||||
from openedx.core.djangoapps.credentials.utils import get_credentials_records_url
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
||||
from openedx.core.djangoapps.user_api.errors import UserNotAuthorized, UserNotFound
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
|
||||
from openedx.features.learner_profile.toggles import should_redirect_to_profile_microfrontend
|
||||
from openedx.features.learner_profile.views.learner_achievements import LearnerAchievementsFragmentView
|
||||
from common.djangoapps.student.models import User
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(['GET'])
|
||||
def learner_profile(request, username):
|
||||
"""Render the profile page for the specified username.
|
||||
|
||||
Args:
|
||||
request (HttpRequest)
|
||||
username (str): username of user whose profile is requested.
|
||||
|
||||
Returns:
|
||||
HttpResponse: 200 if the page was sent successfully
|
||||
HttpResponse: 302 if not logged in (redirect to login page)
|
||||
HttpResponse: 405 if using an unsupported HTTP method
|
||||
Raises:
|
||||
Http404: 404 if the specified user is not authorized or does not exist
|
||||
|
||||
Example usage:
|
||||
GET /account/profile
|
||||
"""
|
||||
if should_redirect_to_profile_microfrontend():
|
||||
profile_microfrontend_url = f"{settings.PROFILE_MICROFRONTEND_URL}{username}"
|
||||
if request.GET:
|
||||
profile_microfrontend_url += f'?{request.GET.urlencode()}'
|
||||
return redirect(profile_microfrontend_url)
|
||||
|
||||
try:
|
||||
context = learner_profile_context(request, username, request.user.is_staff)
|
||||
return render(
|
||||
request=request,
|
||||
template_name='learner_profile/learner_profile.html',
|
||||
context=context
|
||||
)
|
||||
except (UserNotAuthorized, UserNotFound, ObjectDoesNotExist):
|
||||
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
|
||||
def learner_profile_context(request, profile_username, user_is_staff):
|
||||
"""Context for the learner profile page.
|
||||
|
||||
Args:
|
||||
logged_in_user (object): Logged In user.
|
||||
profile_username (str): username of user whose profile is requested.
|
||||
user_is_staff (bool): Logged In user has staff access.
|
||||
build_absolute_uri_func ():
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
Raises:
|
||||
ObjectDoesNotExist: the specified profile_username does not exist.
|
||||
"""
|
||||
profile_user = User.objects.get(username=profile_username)
|
||||
logged_in_user = request.user
|
||||
|
||||
own_profile = (logged_in_user.username == profile_username)
|
||||
|
||||
account_settings_data = get_account_settings(request, [profile_username])[0]
|
||||
|
||||
preferences_data = get_user_preferences(profile_user, profile_username)
|
||||
|
||||
context = {
|
||||
'own_profile': own_profile,
|
||||
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
|
||||
'data': {
|
||||
'profile_user_id': profile_user.id,
|
||||
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
|
||||
'default_visibility': settings.ACCOUNT_VISIBILITY_CONFIGURATION['default_visibility'],
|
||||
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
|
||||
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
|
||||
'preferences_data': preferences_data,
|
||||
'account_settings_data': account_settings_data,
|
||||
'profile_image_upload_url': reverse('profile_image_upload', kwargs={'username': profile_username}),
|
||||
'profile_image_remove_url': reverse('profile_image_remove', kwargs={'username': profile_username}),
|
||||
'profile_image_max_bytes': settings.PROFILE_IMAGE_MAX_BYTES,
|
||||
'profile_image_min_bytes': settings.PROFILE_IMAGE_MIN_BYTES,
|
||||
'account_settings_page_url': reverse('account_settings'),
|
||||
'has_preferences_access': (logged_in_user.username == profile_username or user_is_staff),
|
||||
'own_profile': own_profile,
|
||||
'country_options': list(countries),
|
||||
'find_courses_url': marketing_link('COURSES'),
|
||||
'language_options': settings.ALL_LANGUAGES,
|
||||
'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,
|
||||
'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE,
|
||||
'parental_consent_age_limit': settings.PARENTAL_CONSENT_AGE_LIMIT
|
||||
},
|
||||
'show_program_listing': ProgramsApiConfig.is_enabled(),
|
||||
'show_dashboard_tabs': True,
|
||||
'disable_courseware_js': True,
|
||||
'nav_hidden': True,
|
||||
'records_url': get_credentials_records_url(),
|
||||
}
|
||||
|
||||
if own_profile or user_is_staff:
|
||||
achievements_fragment = LearnerAchievementsFragmentView().render_to_fragment(
|
||||
request,
|
||||
username=profile_user.username,
|
||||
own_profile=own_profile,
|
||||
)
|
||||
context['achievements_fragment'] = achievements_fragment
|
||||
|
||||
return context
|
||||
Reference in New Issue
Block a user