From adf111e4d515abc808375f46b1987fde5ce3dd07 Mon Sep 17 00:00:00 2001
From: Usman Khalid <2200617@gmail.com>
Date: Fri, 3 Apr 2015 15:09:07 +0500
Subject: [PATCH] Adding functionlity to upload/remove profile image.
TNL-1538
---
lms/djangoapps/student_profile/views.py | 4 +
lms/static/js/spec/student_account/helpers.js | 11 +-
.../learner_profile_view_spec.js | 24 ++-
.../models/user_account_model.js | 10 +
.../views/learner_profile_factory.js | 24 ++-
.../views/learner_profile_fields.js | 58 +++++
.../views/learner_profile_view.js | 6 +-
lms/static/js/views/fields.js | 201 ++++++++++++++++++
lms/static/js/views/message_banner.js | 32 +++
lms/static/sass/views/_learner-profile.scss | 79 ++++++-
lms/templates/fields/field_image.underscore | 15 ++
lms/templates/message_banner.underscore | 9 +
.../student_profile/learner_profile.html | 15 +-
.../learner_profile.underscore | 3 +-
14 files changed, 468 insertions(+), 23 deletions(-)
create mode 100644 lms/static/js/views/message_banner.js
create mode 100644 lms/templates/fields/field_image.underscore
create mode 100644 lms/templates/message_banner.underscore
diff --git a/lms/djangoapps/student_profile/views.py b/lms/djangoapps/student_profile/views.py
index e0fa845dca..9ea1138f14 100644
--- a/lms/djangoapps/student_profile/views.py
+++ b/lms/djangoapps/student_profile/views.py
@@ -70,6 +70,10 @@ def learner_profile_context(logged_in_username, profile_username, user_is_staff)
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
+ '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_username == profile_username or user_is_staff),
'own_profile': (logged_in_username == profile_username),
diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js
index 12defc5f7b..c4dc04e48a 100644
--- a/lms/static/js/spec/student_account/helpers.js
+++ b/lms/static/js/spec/student_account/helpers.js
@@ -3,6 +3,8 @@ define(['underscore'], function(_) {
var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student';
var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student';
+ var IMAGE_UPLOAD_API_URL = '/api/profile_images/v0/staff/upload';
+ var IMAGE_REMOVE_API_URL = '/api/profile_images/v0/staff/remove';
var USER_ACCOUNTS_DATA = {
username: 'student',
@@ -27,9 +29,12 @@ define(['underscore'], function(_) {
['0', 'Option 0'],
['1', 'Option 1'],
['2', 'Option 2'],
- ['3', 'Option 3'],
+ ['3', 'Option 3']
];
+ var IMAGE_MAX_BYTES = 1024 * 1024;
+ var IMAGE_MIN_BYTES = 100;
+
var expectLoadingIndicatorIsVisible = function (view, visible) {
if (visible) {
expect($('.ui-loading-indicator')).not.toHaveClass('is-hidden');
@@ -92,6 +97,10 @@ define(['underscore'], function(_) {
return {
USER_ACCOUNTS_API_URL: USER_ACCOUNTS_API_URL,
USER_PREFERENCES_API_URL: USER_PREFERENCES_API_URL,
+ IMAGE_UPLOAD_API_URL: IMAGE_UPLOAD_API_URL,
+ IMAGE_REMOVE_API_URL: IMAGE_REMOVE_API_URL,
+ IMAGE_MAX_BYTES: IMAGE_MAX_BYTES,
+ IMAGE_MIN_BYTES: IMAGE_MIN_BYTES,
USER_ACCOUNTS_DATA: USER_ACCOUNTS_DATA,
USER_PREFERENCES_DATA: USER_PREFERENCES_DATA,
FIELD_OPTIONS: FIELD_OPTIONS,
diff --git a/lms/static/js/spec/student_profile/learner_profile_view_spec.js b/lms/static/js/spec/student_profile/learner_profile_view_spec.js
index a1d6dd09cd..00826017a5 100644
--- a/lms/static/js/spec/student_profile/learner_profile_view_spec.js
+++ b/lms/static/js/spec/student_profile/learner_profile_view_spec.js
@@ -6,11 +6,12 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
'js/student_account/models/user_preferences_model',
'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view',
- 'js/student_account/views/account_settings_fields'
+ 'js/student_account/views/account_settings_fields',
+ 'js/views/message_banner'
],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
- AccountSettingsFieldViews) {
+ AccountSettingsFieldViews, MessageBannerView) {
'use strict';
describe("edx.user.LearnerProfileView", function () {
@@ -46,6 +47,21 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
accountSettingsPageUrl: '/account/settings/'
});
+ var messageView = new MessageBannerView({
+ el: $('.message-banner')
+ });
+
+ var profileImageFieldView = new FieldsView.ImageFieldView({
+ 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",
@@ -107,10 +123,12 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
};
beforeEach(function () {
- setFixtures('
An error occurred. Please reload the page.
');
+ setFixtures('
An error occurred. Please reload the page.
');
TemplateHelpers.installTemplate('templates/fields/field_readonly');
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
TemplateHelpers.installTemplate('templates/fields/field_textarea');
+ TemplateHelpers.installTemplate('templates/fields/field_image');
+ TemplateHelpers.installTemplate('templates/message_banner');
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
});
diff --git a/lms/static/js/student_account/models/user_account_model.js b/lms/static/js/student_account/models/user_account_model.js
index ca8d9e4b29..e847d0ba88 100644
--- a/lms/static/js/student_account/models/user_account_model.js
+++ b/lms/static/js/student_account/models/user_account_model.js
@@ -22,6 +22,7 @@
bio: null,
language_proficiencies: [],
requires_parental_consent: true,
+ profile_image: null,
default_public_account_fields: []
},
@@ -39,6 +40,15 @@
return response;
},
+ hasProfileImage: function () {
+ var profile_image = this.get('profile_image');
+ return (_.isObject(profile_image) && profile_image['has_image'] === true);
+ },
+
+ profileImageUrl: function () {
+ return this.get('profile_image')['image_url_large'];
+ },
+
isAboveMinimumAge: function() {
var isBirthDefined = !(_.isUndefined(this.get('year_of_birth')) || _.isNull(this.get('year_of_birth')));
return isBirthDefined && !(this.get("requires_parental_consent"));
diff --git a/lms/static/js/student_profile/views/learner_profile_factory.js b/lms/static/js/student_profile/views/learner_profile_factory.js
index 2f36c92ce7..8f3a9f26a1 100644
--- a/lms/static/js/student_profile/views/learner_profile_factory.js
+++ b/lms/static/js/student_profile/views/learner_profile_factory.js
@@ -7,9 +7,10 @@
'js/views/fields',
'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view',
- 'js/student_account/views/account_settings_fields'
- ], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
- LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews) {
+ 'js/student_account/views/account_settings_fields',
+ 'js/views/message_banner'
+ ], function (gettext, $, _, Backbone, AccountSettingsModel, AccountPreferencesModel, FieldsView,
+ LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageBannerView) {
return function (options) {
@@ -25,6 +26,10 @@
var editable = options.own_profile ? 'toggle' : 'never';
+ var messageView = new MessageBannerView({
+ el: $('.message-banner')
+ });
+
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
model: accountPreferencesModel,
required: true,
@@ -40,6 +45,17 @@
accountSettingsPageUrl: options.account_settings_page_url
});
+ var 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']
+ });
+
var usernameFieldView = new FieldsView.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: "username",
@@ -47,7 +63,6 @@
});
var sectionOneFieldViews = [
- usernameFieldView,
new FieldsView.DropdownFieldView({
model: accountSettingsModel,
required: true,
@@ -94,6 +109,7 @@
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
+ profileImageFieldView: profileImageFieldView,
usernameFieldView: usernameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews
diff --git a/lms/static/js/student_profile/views/learner_profile_fields.js b/lms/static/js/student_profile/views/learner_profile_fields.js
index bae5aa7116..e5c6e13d61 100644
--- a/lms/static/js/student_profile/views/learner_profile_fields.js
+++ b/lms/static/js/student_profile/views/learner_profile_fields.js
@@ -48,6 +48,64 @@
}
});
+ LearnerProfileFieldViews.ProfileImageFieldView = FieldViews.ImageFieldView.extend({
+
+ imageUrl: function () {
+ return this.model.profileImageUrl();
+ },
+
+ imageAltText: function () {
+ return interpolate_text(gettext("Profile image for {username}"), {username: this.model.get('username')});
+ },
+
+ imageChangeSucceeded: function (e, data) {
+ var view = this;
+ // Update model to get the latest urls of profile image.
+ this.model.fetch().done(function () {
+ view.setCurrentStatus('');
+ }).fail(function () {
+ view.showErrorMessage(view.errorMessage);
+ });
+ },
+
+ imageChangeFailed: function (e, data) {
+ this.setCurrentStatus('');
+ if (_.contains([400, 404], data.jqXHR.status)) {
+ try {
+ var errors = JSON.parse(data.jqXHR.responseText);
+ this.showErrorMessage(errors.user_message);
+ } catch (error) {
+ this.showErrorMessage(this.errorMessage);
+ }
+ } else {
+ this.showErrorMessage(this.errorMessage);
+ }
+ this.render();
+ },
+
+ 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);
+ }
+ });
+
return LearnerProfileFieldViews;
});
}).call(this, define || RequireJS.define);
diff --git a/lms/static/js/student_profile/views/learner_profile_view.js b/lms/static/js/student_profile/views/learner_profile_view.js
index f52a9262b8..36f477b6d7 100644
--- a/lms/static/js/student_profile/views/learner_profile_view.js
+++ b/lms/static/js/student_profile/views/learner_profile_view.js
@@ -24,7 +24,6 @@
render: function () {
this.$el.html(this.template({
username: this.options.accountSettingsModel.get('username'),
- profilePhoto: 'http://www.teachthought.com/wp-content/uploads/2012/07/edX-120x120.jpg',
ownProfile: this.options.ownProfile,
showFullProfile: this.showFullProfile()
}));
@@ -48,6 +47,11 @@
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
+ var imageView = this.options.profileImageFieldView;
+ imageView.undelegateEvents();
+ this.$('.profile-image-field').append(imageView.render().el);
+ imageView.delegateEvents();
+
if (this.showFullProfile()) {
_.each(this.options.sectionOneFieldViews, function (fieldView) {
fieldView.undelegateEvents();
diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js
index b14b263c0c..2a8c1b7619 100644
--- a/lms/static/js/views/fields.js
+++ b/lms/static/js/views/fields.js
@@ -499,6 +499,207 @@
}
});
+ FieldViews.ImageFieldView = FieldViews.FieldView.extend({
+
+ fieldType: 'image',
+
+ templateSelector: '#field_image-tpl',
+ uploadButtonSelector: '.upload-button-input',
+
+ titleAdd: gettext("Upload an image"),
+ titleEdit: gettext("Change image"),
+ titleRemove: gettext("Remove"),
+
+ titleUploading: gettext("Uploading"),
+ titleRemoving: gettext("Removing"),
+
+ titleImageAlt: '',
+
+ iconUpload: ' ',
+ iconRemove: ' ',
+ iconProgress: ' ',
+
+ errorMessage: gettext("An error has occurred. Refresh the page, and then try again."),
+
+ events: {
+ 'click .u-field-upload-button': 'clickedUploadButton',
+ 'click .u-field-remove-button': 'clickedRemoveButton'
+ },
+
+ initialize: function (options) {
+ this._super(options);
+ _.bindAll(this, 'render', 'imageChangeSucceeded', 'imageChangeFailed', 'fileSelected',
+ 'watchForPageUnload', 'onBeforeUnload');
+ this.listenTo(this.model, "change:" + this.options.valueAttribute, this.render);
+ },
+
+ render: function () {
+ this.$el.html(this.template({
+ id: this.options.valueAttribute,
+ imageUrl: _.result(this, 'imageUrl'),
+ imageAltText: _.result(this, 'imageAltText'),
+ uploadButtonIcon: _.result(this, 'iconUpload'),
+ uploadButtonTitle: _.result(this, 'uploadButtonTitle'),
+ removeButtonIcon: _.result(this, 'iconRemove'),
+ removeButtonTitle: _.result(this, 'removeButtonTitle')
+ }));
+ this.updateButtonsVisibility();
+ this.watchForPageUnload();
+ return this;
+ },
+
+ showErrorMessage: function () {
+ },
+
+ imageUrl: function () {
+ return '';
+ },
+
+ uploadButtonTitle: function () {
+ if (this.isShowingPlaceholder()) {
+ return _.result(this, 'titleAdd')
+ } else {
+ return _.result(this, 'titleEdit')
+ }
+ },
+
+ removeButtonTitle: function () {
+ return this.titleRemove;
+ },
+
+ isEditingAllowed: function () {
+ return true
+ },
+
+ isShowingPlaceholder: function () {
+ return false;
+ },
+
+ setUploadButtonVisibility: function (state) {
+ this.$('.u-field-upload-button').css('display', state);
+ },
+
+ setRemoveButtonVisibility: function (state) {
+ this.$('.u-field-remove-button').css('display', state);
+ },
+
+ updateButtonsVisibility: function () {
+ if (!this.isEditingAllowed() || !this.options.editable) {
+ this.setUploadButtonVisibility('none');
+ }
+
+ if (this.isShowingPlaceholder() || !this.options.editable) {
+ this.setRemoveButtonVisibility('none');
+ }
+ },
+
+ clickedUploadButton: function () {
+ $(this.uploadButtonSelector).fileupload({
+ url: this.options.imageUploadUrl,
+ type: 'POST',
+ add: this.fileSelected,
+ done: this.imageChangeSucceeded,
+ fail: this.imageChangeFailed
+ });
+ },
+
+ clickedRemoveButton: function () {
+ var view = this;
+ this.setCurrentStatus('removing');
+ this.setUploadButtonVisibility('none');
+ this.showRemovalInProgressMessage();
+ $.ajax({
+ type: 'POST',
+ url: this.options.imageRemoveUrl,
+ success: function (data, status, xhr) {
+ view.imageChangeSucceeded();
+ },
+ error: function (xhr, status, error) {
+ view.imageChangeFailed();
+ }
+ });
+ },
+
+ imageChangeSucceeded: function (e, data) {
+ this.render();
+ },
+
+ imageChangeFailed: function (e, data) {
+ },
+
+ fileSelected: function (e, data) {
+ if (this.validateImageSize(data.files[0].size)) {
+ data.formData = {file: data.files[0]};
+ this.setCurrentStatus('uploading');
+ this.setRemoveButtonVisibility('none');
+ this.showUploadInProgressMessage();
+ data.submit();
+ }
+ },
+
+ validateImageSize: function (imageBytes) {
+ var humanReadableSize;
+ if (imageBytes < this.options.imageMinBytes) {
+ humanReadableSize = this.bytesToHumanReadable(this.options.imageMinBytes);
+ this.showErrorMessage(interpolate_text(gettext("Your image must be at least {size} in size."), {size: humanReadableSize}));
+ return false;
+ } else if (imageBytes > this.options.imageMaxBytes) {
+ humanReadableSize = this.bytesToHumanReadable(this.options.imageMaxBytes);
+ this.showErrorMessage(interpolate_text(gettext("Your image must be smaller than {size} in size."), {size: humanReadableSize}));
+ return false;
+ }
+ return true;
+ },
+
+ showUploadInProgressMessage: function () {
+ this.$('.u-field-upload-button').css('opacity', 1);
+ this.$('.upload-button-icon').html(this.iconProgress);
+ this.$('.upload-button-title').html(this.titleUploading);
+ },
+
+ showRemovalInProgressMessage: function () {
+ this.$('.u-field-remove-button').css('opacity', 1);
+ this.$('.remove-button-icon').html(this.iconProgress);
+ this.$('.remove-button-title').html(this.titleRemoving);
+ },
+
+ setCurrentStatus: function (status) {
+ this.$('.image-wrapper').attr('data-status', status);
+ },
+
+ getCurrentStatus: function () {
+ return this.$('.image-wrapper').attr('data-status');
+ },
+
+ inProgress: function() {
+ var status = this.getCurrentStatus();
+ return _.isUndefined(status) ? false : true;
+ },
+
+ watchForPageUnload: function () {
+ $(window).on('beforeunload', this.onBeforeUnload);
+ },
+
+ onBeforeUnload: function () {
+ var status = this.getCurrentStatus();
+ if (status === 'uploading') {
+ return gettext("Upload is in progress. To avoid errors, stay on this page until the process is complete.");
+ } else if (status === 'removing') {
+ return gettext("Removal is in progress. To avoid errors, stay on this page until the process is complete.");
+ }
+ },
+
+ bytesToHumanReadable: function (size) {
+ var units = ['Bytes', 'KB', 'MB'];
+ var i = 0;
+ while(size >= 1024) {
+ size /= 1024;
+ ++i;
+ }
+ return size.toFixed(1)*1 + ' ' + units[i];
+ }
+ });
+
return FieldViews;
});
}).call(this, define || RequireJS.define);
diff --git a/lms/static/js/views/message_banner.js b/lms/static/js/views/message_banner.js
new file mode 100644
index 0000000000..6227f8d7db
--- /dev/null
+++ b/lms/static/js/views/message_banner.js
@@ -0,0 +1,32 @@
+;(function (define, undefined) {
+ 'use strict';
+ define([
+ 'gettext', 'jquery', 'underscore', 'backbone'
+ ], function (gettext, $, _, Backbone) {
+
+ var MessageBannerView = Backbone.View.extend({
+
+ initialize: function (options) {
+ this.template = _.template($('#message_banner-tpl').text());
+ },
+
+ render: function () {
+ this.$el.html(this.template({
+ message: this.message
+ }));
+ return this;
+ },
+
+ showMessage: function (message) {
+ this.message = message;
+ this.render();
+ },
+
+ hideMessage: function () {
+ this.$el.html('');
+ }
+ });
+
+ return MessageBannerView;
+ })
+}).call(this, define || RequireJS.define);
diff --git a/lms/static/sass/views/_learner-profile.scss b/lms/static/sass/views/_learner-profile.scss
index b02a663b45..06ea05ea3c 100644
--- a/lms/static/sass/views/_learner-profile.scss
+++ b/lms/static/sass/views/_learner-profile.scss
@@ -7,7 +7,7 @@
// * +Settings Section
.view-profile {
- $profile-photo-dimension: 120px;
+ $profile-image-dimension: 120px;
.content-wrapper {
background-color: $white;
@@ -23,6 +23,75 @@
width: ($baseline*5);
}
+ .profile-image-field {
+ @include float(left);
+
+ button {
+ background: transparent !important;
+ border: none !important;
+ padding: 0;
+ }
+
+ .u-field-image {
+ padding-top: 0;
+ }
+
+ .image-wrapper {
+ width: $profile-image-dimension;
+ position: relative;
+
+ .image-frame {
+ position: relative;
+ width: 120px;
+ height: 120px;
+ }
+
+ .u-field-upload-button {
+ width: 120px;
+ height: 120px;
+ position: absolute;
+ top: 0;
+ opacity: 0;
+
+ i {
+ color: $white;
+ }
+ }
+
+ .upload-button-icon, .upload-button-title {
+ text-align: center;
+ transform: translateY(45px);
+ display: block;
+ color: $white;
+ }
+
+ .upload-button-input {
+ width: 120px;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ cursor: pointer;
+ }
+
+ .u-field-remove-button {
+ width: 120px;
+ height: 20px;
+ opacity: 0;
+ position: relative;
+ margin-top: 2px;
+ text-align: center;
+ }
+
+ &:hover {
+ .u-field-upload-button, .u-field-remove-button {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
.wrapper-profile {
min-height: 200px;
@@ -77,14 +146,6 @@
width: 100%;
display: inline-block;
margin-top: ($baseline*1.5);
-
- .profile-photo {
- @include float(left);
- height: $profile-photo-dimension;
- width: $profile-photo-dimension;
- display: inline-block;
- vertical-align: top;
- }
}
.profile-section-one-fields {
diff --git a/lms/templates/fields/field_image.underscore b/lms/templates/fields/field_image.underscore
new file mode 100644
index 0000000000..5c345ada5e
--- /dev/null
+++ b/lms/templates/fields/field_image.underscore
@@ -0,0 +1,15 @@
+
+
+
+
+ <%= uploadButtonIcon %>
+ <%= uploadButtonTitle %>
+
+
+
+
+ <%= removeButtonIcon %>
+ <%= removeButtonTitle %>
+
+
+
\ No newline at end of file
diff --git a/lms/templates/message_banner.underscore b/lms/templates/message_banner.underscore
new file mode 100644
index 0000000000..c2cd316ccf
--- /dev/null
+++ b/lms/templates/message_banner.underscore
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/lms/templates/student_profile/learner_profile.html b/lms/templates/student_profile/learner_profile.html
index 17be327cad..29475aa614 100644
--- a/lms/templates/student_profile/learner_profile.html
+++ b/lms/templates/student_profile/learner_profile.html
@@ -10,27 +10,36 @@
<%block name="bodyclass">view-profile%block>
<%block name="header_extras">
- % for template_name in ["field_dropdown", "field_textarea", "field_readonly"]:
+ % for template_name in ["field_dropdown", "field_image", "field_textarea", "field_readonly"]:
% endfor
- % for template_name in ["learner_profile",]:
+ % for template_name in ["learner_profile"]:
% endfor
+
+ % for template_name in ["message_banner"]:
+
+ % endfor
%block>
+
-
${_("Loading")}
+
${_("Loading")}
<%block name="headextra">
<%static:css group='style-course'/>
+
+