diff --git a/.gitignore b/.gitignore index fa417911e5..637b00b853 100644 --- a/.gitignore +++ b/.gitignore @@ -107,8 +107,7 @@ cms/static/css/ cms/static/sass/*.css cms/static/sass/*.css.map cms/static/themed_sass/ -themes/**/css/*.css -themes/**/css/discussion/*.css +themes/**/css ### Logging artifacts log/ diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index dacbec78d5..83f6c71b46 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -245,6 +245,7 @@ def cert_info(user, course_overview, course_mode): """ if not course_overview.may_certify(): return {} + # Note: this should be rewritten to use the certificates API return _cert_info( user, course_overview, diff --git a/lms/static/images/certificates/audit.png b/lms/static/images/certificates/audit.png new file mode 100644 index 0000000000..7363f01845 Binary files /dev/null and b/lms/static/images/certificates/audit.png differ diff --git a/lms/static/images/certificates/honor.png b/lms/static/images/certificates/honor.png new file mode 100644 index 0000000000..7363f01845 Binary files /dev/null and b/lms/static/images/certificates/honor.png differ diff --git a/lms/static/images/certificates/professional.png b/lms/static/images/certificates/professional.png new file mode 100644 index 0000000000..7363f01845 Binary files /dev/null and b/lms/static/images/certificates/professional.png differ diff --git a/lms/static/images/certificates/verified.png b/lms/static/images/certificates/verified.png new file mode 100644 index 0000000000..7363f01845 Binary files /dev/null and b/lms/static/images/certificates/verified.png differ diff --git a/lms/static/sass/_build-base-v1-rtl.scss b/lms/static/sass/_build-base-v1-rtl.scss index 7396056d54..621fe4fdb1 100644 --- a/lms/static/sass/_build-base-v1-rtl.scss +++ b/lms/static/sass/_build-base-v1-rtl.scss @@ -7,3 +7,4 @@ @import 'base/reset'; @import 'base/variables'; @import 'base/mixins'; +@import 'base/theme'; diff --git a/lms/static/sass/_build-base-v1.scss b/lms/static/sass/_build-base-v1.scss index 30979a5af9..0c63cb6c34 100644 --- a/lms/static/sass/_build-base-v1.scss +++ b/lms/static/sass/_build-base-v1.scss @@ -6,3 +6,4 @@ @import 'base/reset'; @import 'base/variables'; @import 'base/mixins'; +@import 'base/theme'; diff --git a/lms/static/sass/_build-footer-edx.scss b/lms/static/sass/_build-footer-edx.scss index 09b539b5c5..51546929dc 100644 --- a/lms/static/sass/_build-footer-edx.scss +++ b/lms/static/sass/_build-footer-edx.scss @@ -4,6 +4,7 @@ // base - utilities @import 'base/variables'; @import 'base/mixins'; +@import 'base/theme'; footer#footer-edx-v3 { @import 'base/extends'; diff --git a/lms/static/sass/features/_learner-profile.scss b/lms/static/sass/features/_learner-profile.scss index 33a1305462..f9d1997142 100644 --- a/lms/static/sass/features/_learner-profile.scss +++ b/lms/static/sass/features/_learner-profile.scss @@ -1,10 +1,102 @@ // lms - application - learner profile // ==================== -// Table of Contents -// * +Container - Learner Profile -// * +Main - Header -// * +Settings Section +.learner-achievements { + .learner-message { + @extend %no-content; + margin: $baseline*0.75 0; + + .message-header, .message-actions { + text-align: center; + } + + .message-actions { + margin-top: $baseline/2; + + .btn-brand { + color: $white; + } + } + } +} + +.certificate-card { + display: flex; + flex-direction: row; + margin-bottom: $baseline; + padding: $baseline/2; + border: 1px; + border-style: solid; + background-color: $white; + cursor: pointer; + + &:hover { + box-shadow: 0 0 1px 1px $gray-l2; + } + + .card-logo { + @include margin-right($baseline); + width: 100px; + height: 100px; + + @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap + display: none; + } + } + + .card-content { + color: $base-font-color; + margin-top: $baseline/2; + } + + .card-supertitle { + @extend %t-title6; + color: $lightest-base-font-color; + } + + .card-title { + @extend %t-title5; + @extend %t-strong; + margin-bottom: $baseline/2; + } + + .card-text { + @extend %t-title8; + color: $lightest-base-font-color; + } + + &.mode-audit { + border-color: $audit-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/audit.png'); + } + } + + &.mode-honor { + border-color: $honor-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/honor.png'); + } + } + + &.mode-verified { + border-color: $verified-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/verified.png'); + } + } + + &.mode-professional { + border-color: $professional-certificate-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/professional.png'); + } + } +} .view-profile { $profile-image-dimension: 120px; @@ -210,133 +302,146 @@ } } - .wrapper-profile-section-one { - @include float(left); - @include margin-left($baseline*3); - width: 300px; - background-color: $white; - border-top: 5px solid $blue; - padding-bottom: $baseline; - + .wrapper-profile-section-container-one { @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - @include margin-left(0); - width: 100%; + width: 90%; + padding: 0 5%; + } + + .wrapper-profile-section-one { + @include float(left); + @include margin-left($baseline*3); + width: 300px; + background-color: $white; + border-top: 5px solid $blue; + padding-bottom: $baseline; + + @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap + @include margin-left(0); + width: 100%; + } + + .profile-section-one-fields { + margin: 0 $baseline/2; + + .social-links { + font-size: 2rem; + padding-top: $baseline/4; + + & > span { + color: $gray-l4; + } + + a { + .fa-facebook-square { + color: $facebook-blue; + } + + .fa-twitter-square { + color: $twitter-blue; + } + + .fa-linkedin-square { + color: $linkedin-blue; + } + } + } + + .u-field { + @extend %t-weight4; + @include padding(0, 0, 0, 3px); + color: $base-font-color; + margin-top: $baseline/5; + + .u-field-value, .u-field-title { + @extend %t-weight4; + width: calc(100% - 40px); + } + + .u-field-value-readonly { + @extend %t-weight3; + font-family: $sans-serif; + color: $darkest-base-font-color; + } + + .u-field-title { + color: $lightest-base-font-color; + display: block; + } + + &.u-field-dropdown { + position: relative; + + &:not(.editable-never) { + cursor: pointer; + } + + &:not(:last-child) { + padding-bottom: $baseline/4; + border-bottom: 1px solid $gray-lighter; + + &:hover.mode-placeholder { + padding-bottom: $baseline/5; + border-bottom: 2px dashed $link-color; + } + } + } + } + + &>.u-field { + &:not(:first-child) { + font-size: $body-font-size; + color: $base-font-color; + font-weight: $font-light; + margin-bottom: 0; + } + + &:first-child { + @extend %t-title4; + @extend %t-weight4; + font-size: em(24); + } + } + + select { + width: 85% + } + + .u-field-message { + @include right(0); + position: absolute; + top: 0; + width: 20px; + + .icon { + vertical-align: baseline; + } + } + } } } - .profile-section-one-fields { - @include margin(0, $baseline/2, 0, $baseline*0.75); - - .social-links { - font-size: 2rem; - padding-top: $baseline/4; - - & > span { - color: $gray-l4; - } - - a { - .fa-facebook-square { - color: $facebook-blue; - } - - .fa-twitter-square { - color: $twitter-blue; - } - - .fa-linkedin-square { - color: $linkedin-blue; - } - } - } - - .u-field { - @extend %t-weight4; - padding: 0; - color: $base-font-color; - margin-top: $baseline/5; - - .u-field-value, .u-field-title { - @extend %t-weight4; - width: calc(100% - 40px); - } - - .u-field-value-readonly { - font-weight: 500; - font-family: $sans-serif; - color: $darkest-base-font-color; - } - - .u-field-title { - color: $lightest-base-font-color; - display: block; - } - - &:not(.u-field-readonly):not(:last-child) { - padding-bottom: $baseline/4; - border-bottom: 1px solid $gray-lighter; - - &:hover.mode-placeholder { - padding-bottom: $baseline/5; - border-bottom: 2px dashed $link-color; - } - } - &.u-field-dropdown { - position: relative; - - &:not(.editable-never) { - cursor: pointer; - } - - } - } - - &>.u-field { - &:not(:first-child) { - font-size: $body-font-size; - color: $base-font-color; - font-weight: $font-light; - margin-bottom: 0; - } - - &:first-child { - @extend %t-title4; - @extend %t-weight4; - font-size: em(24); - } - } - - select { - width: 85% - } - - .u-field-message { - @include right(0); - position: absolute; - top: 0; - width: 20px; - - .icon { - vertical-align: baseline; - } - } - } .wrapper-profile-section-container-two { @include float(left); @include padding-left($baseline); width: calc(100% - 380px); max-width: $learner-profile-container-flex; // Switch to map-get($grid-breakpoints,md) for bootstrap + font-family: $sans-serif; @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap - @include padding-left(0); - width: 100%; + width: 90%; margin-top: $baseline; + padding: 0 5%; } .u-field-textarea { + @include padding(0, ($baseline*.75), ($baseline*.75), 0); margin-bottom: ($baseline/2); - @include padding(0, ($baseline*.75), ($baseline*.75), ($baseline/4)); + + @media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap + @include padding-left($baseline/4); + } .u-field-header { position: relative; @@ -355,12 +460,11 @@ .u-field-title { @extend %t-title6; - @extend %t-weight5; display: inline-block; margin-top: 0; margin-bottom: ($baseline/4); - color: $gray-dark; width: 100%; + font: $font-semibold 1.4em/1.4em $sans-serif; } .u-field-value { @@ -396,7 +500,7 @@ .u-field.mode-placeholder { padding: $baseline; - margin: $baseline * 0.75; + margin: $baseline*0.75 0; border: 2px dashed $gray-l3; i { diff --git a/lms/static/sass/lms-course-rtl.scss b/lms/static/sass/lms-course-rtl.scss index 654565bd98..d7b20b2b3d 100644 --- a/lms/static/sass/lms-course-rtl.scss +++ b/lms/static/sass/lms-course-rtl.scss @@ -5,5 +5,6 @@ @import 'base/variables'; @import 'base/font_face'; @import 'base/mixins'; +@import 'base/theme'; @import 'build-course'; // shared app style assets/rendering diff --git a/lms/static/sass/lms-course.scss b/lms/static/sass/lms-course.scss index 8ebd978a8e..4faa14b9f4 100644 --- a/lms/static/sass/lms-course.scss +++ b/lms/static/sass/lms-course.scss @@ -5,5 +5,6 @@ @import 'base/variables'; @import 'base/font_face'; @import 'base/mixins'; +@import 'base/theme'; @import 'build-course'; // shared app style assets/rendering diff --git a/lms/static/sass/lms-footer-edx-rtl.scss b/lms/static/sass/lms-footer-edx-rtl.scss index e189fe9e6d..e435906758 100644 --- a/lms/static/sass/lms-footer-edx-rtl.scss +++ b/lms/static/sass/lms-footer-edx-rtl.scss @@ -7,4 +7,4 @@ @import 'base/variables-rtl'; // Import shared build for the edx.org footer -@import 'build-footer-edx' +@import 'build-footer-edx'; diff --git a/lms/static/sass/lms-footer-edx.scss b/lms/static/sass/lms-footer-edx.scss index afa3d26592..44f54ccd87 100644 --- a/lms/static/sass/lms-footer-edx.scss +++ b/lms/static/sass/lms-footer-edx.scss @@ -7,4 +7,4 @@ @import 'base/variables-ltr'; // Import shared build for the edx.org footer -@import 'build-footer-edx' +@import 'build-footer-edx'; diff --git a/lms/static/sass/lms-footer-rtl.scss b/lms/static/sass/lms-footer-rtl.scss index ad15a1c9aa..ea5f2ab9ea 100644 --- a/lms/static/sass/lms-footer-rtl.scss +++ b/lms/static/sass/lms-footer-rtl.scss @@ -8,6 +8,7 @@ // base - utilities @import 'base/variables'; @import 'base/mixins'; +@import 'base/theme'; footer#footer-openedx { @import 'base/reset'; diff --git a/lms/static/sass/lms-footer.scss b/lms/static/sass/lms-footer.scss index 9173c23b7e..975c1a211a 100644 --- a/lms/static/sass/lms-footer.scss +++ b/lms/static/sass/lms-footer.scss @@ -8,6 +8,7 @@ // base - utilities @import 'base/variables'; @import 'base/mixins'; +@import 'base/theme'; footer#footer-openedx { @import 'base/reset'; diff --git a/lms/static/sass/partials/base/_theme.scss b/lms/static/sass/partials/base/_theme.scss new file mode 100644 index 0000000000..c1e4fb2230 --- /dev/null +++ b/lms/static/sass/partials/base/_theme.scss @@ -0,0 +1 @@ +// File to be overridden by themes diff --git a/lms/static/sass/partials/base/_variables.scss b/lms/static/sass/partials/base/_variables.scss index 1a86d447a0..d637e93d60 100644 --- a/lms/static/sass/partials/base/_variables.scss +++ b/lms/static/sass/partials/base/_variables.scss @@ -248,11 +248,14 @@ $state-danger-border: darken($state-danger-bg, 5%) !default; // ---------------------------- // logo colors -$micromasters-color: #005585; -$xseries-color: #424242; -$professional-certificate-color: #9a1f60; -$zebra-stripe-color: rgb(249, 250, 252); -$divider-color: rgb(226,231,236); +$audit-mode-color: $gray-dark !default; +$honor-mode-color: $uxpl-blue-base !default; +$verified-mode-color: $uxpl-green-base !default; +$micromasters-color: #005585 !default; +$xseries-color: #424242 !default; +$professional-certificate-color: #9a1f60 !default; +$zebra-stripe-color: rgb(249, 250, 252) !default; +$divider-color: rgb(226,231,236) !default; // old color variables // DEPRECATED: Do not continue to use these colors, instead use pattern libary and base colors above. diff --git a/openedx/features/learner_profile/__init__.py b/openedx/features/learner_profile/__init__.py index e69de29bb2..d1d21fdd14 100644 --- a/openedx/features/learner_profile/__init__.py +++ b/openedx/features/learner_profile/__init__.py @@ -0,0 +1,13 @@ +""" +Learner profile settings and helper methods. +""" + +from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace + + +# Namespace for learner profile waffle flags. +WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='learner_profile') + +# Waffle flag to show achievements on the learner profile. +# TODO: LEARNER-2443: 08/2017: Remove flag after rollout. +SHOW_ACHIEVEMENTS_FLAG = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_achievements') diff --git a/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html b/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html index d3719597f5..61c139210a 100644 --- a/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html +++ b/openedx/features/learner_profile/static/learner_profile/fixtures/learner_profile.html @@ -1,19 +1,40 @@
-
-

+

+ + +
+
+

- + Loading -

-
- + +
diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js index 9f943e5214..d3ebdc6d26 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js +++ b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js @@ -36,14 +36,14 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' }; var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) { - var accountPrivacyElement = learnerProfileView.$('.wrapper-profile-field-account-privacy'); - var privacyFieldElement = $(accountPrivacyElement).find('.u-field'); + var $accountPrivacyElement = $('.wrapper-profile-field-account-privacy'); + var $privacyFieldElement = $($accountPrivacyElement).find('.u-field'); if (othersProfile) { - expect(privacyFieldElement.length).toBe(0); + expect($privacyFieldElement.length).toBe(0); } else { - expect(privacyFieldElement.length).toBe(1); - expectProfileElementContainsField(privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView); + expect($privacyFieldElement.length).toBe(1); + expectProfileElementContainsField($privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView); } }; @@ -65,12 +65,12 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' }; var expectSectionTwoTobeRendered = function(learnerProfileView) { - var sectionTwoElement = learnerProfileView.$('.wrapper-profile-section-two'); - var sectionTwoFieldElements = $(sectionTwoElement).find('.u-field'); + var $sectionTwoElement = $('.wrapper-profile-section-two'); + var $sectionTwoFieldElements = $($sectionTwoElement).find('.u-field'); - expect(sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length); + expect($sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length); - _.each(sectionTwoFieldElements, function(sectionFieldElement, fieldIndex) { + _.each($sectionTwoFieldElements, function(sectionFieldElement, fieldIndex) { expectProfileElementContainsField( sectionFieldElement, learnerProfileView.options.sectionTwoFieldViews[fieldIndex] @@ -85,7 +85,7 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' }; var expectLimitedProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) { - var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field'); + var sectionOneFieldElements = $('.wrapper-profile-section-one').find('.u-field'); expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile); @@ -108,9 +108,9 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' }; var expectProfileSectionsNotToBeRendered = function(learnerProfileView) { - expect(learnerProfileView.$('.wrapper-profile-field-account-privacy').length).toBe(0); - expect(learnerProfileView.$('.wrapper-profile-section-one').length).toBe(0); - expect(learnerProfileView.$('.wrapper-profile-section-two').length).toBe(0); + 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) { @@ -124,42 +124,42 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' }; var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) { - var badgeListingView = learnerProfileView.$el.find('#tabpanel-accomplishments'), + var $badgeListingView = $('#tabpanel-accomplishments'), updatedLength = length, placeholder; - expect(learnerProfileView.$el.find('#tabpanel-about_me').hasClass('is-hidden')).toBe(true); - expect(badgeListingView.hasClass('is-hidden')).toBe(false); + expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(true); + expect($badgeListingView.hasClass('is-hidden')).toBe(false); if (lastPage) { updatedLength += 1; - placeholder = badgeListingView.find('.find-course'); + placeholder = $badgeListingView.find('.find-course'); expect(placeholder.length).toBe(1); expect(placeholder.attr('href')).toBe('/courses/'); } - expect(badgeListingView.find('.badge-display').length).toBe(updatedLength); + expect($badgeListingView.find('.badge-display').length).toBe(updatedLength); }; var expectBadgesHidden = function(learnerProfileView) { - var accomplishmentsTab = learnerProfileView.$el.find('#tabpanel-accomplishments'); - if (accomplishmentsTab.length) { + var $accomplishmentsTab = $('#tabpanel-accomplishments'); + if ($accomplishmentsTab.length) { // Nonexistence counts as hidden. - expect(learnerProfileView.$el.find('#tabpanel-accomplishments').hasClass('is-hidden')).toBe(true); + expect($('#tabpanel-accomplishments').hasClass('is-hidden')).toBe(true); } - expect(learnerProfileView.$el.find('#tabpanel-about_me').hasClass('is-hidden')).toBe(false); + expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(false); }; var expectPage = function(learnerProfileView, pageData) { - var badgeListContainer = learnerProfileView.$el.find('#tabpanel-accomplishments'); - var index = badgeListContainer.find('span.search-count').text().trim(); + var $badgeListContainer = $('#tabpanel-accomplishments'); + var index = $badgeListContainer.find('span.search-count').text().trim(); expect(index).toBe('Showing ' + (pageData.start + 1) + '-' + (pageData.start + pageData.results.length) + ' out of ' + pageData.count + ' total'); - expect(badgeListContainer.find('.current-page').text()).toBe('' + pageData.current_page); + expect($badgeListContainer.find('.current-page').text()).toBe('' + pageData.current_page); _.each(pageData.results, function(badge) { expect($('.badge-display:contains(' + badge.badge_class.display_name + ')').length).toBe(1); }); }; var expectBadgeLoadingErrorIsRendered = function(learnerProfileView) { - var errorMessage = learnerProfileView.$el.find('.badge-set-display').text(); + var errorMessage = $('.badge-set-display').text(); expect(errorMessage).toBe( 'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' + 'Help tab to report the problem.' diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js index 96a8e55cdb..9c8627baca 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js +++ b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js @@ -5,11 +5,9 @@ [ 'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils', 'common/js/components/views/tabbed_view', - 'learner_profile/js/views/section_two_tab', - 'text!learner_profile/templates/learner_profile.underscore', - 'edx-ui-toolkit/js/utils/string-utils' + 'learner_profile/js/views/section_two_tab' ], - function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab, learnerProfileTemplate, StringUtils) { + function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab) { var LearnerProfileView = Backbone.View.extend({ initialize: function(options) { @@ -25,8 +23,6 @@ this.firstRender = true; }, - template: _.template(learnerProfileTemplate), - showFullProfile: function() { var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge(); if (this.options.ownProfile) { @@ -54,22 +50,13 @@ ownProfile: this.options.ownProfile }); - - HtmlUtils.setHtml(this.$el, HtmlUtils.template(learnerProfileTemplate)({ - username: self.options.accountSettingsModel.get('username'), - name: self.options.accountSettingsModel.get('name'), - ownProfile: self.options.ownProfile, - showFullProfile: self.showFullProfile(), - profile_header: gettext('My Profile'), - profile_subheader: - StringUtils.interpolate( - gettext('Build out your profile to personalize your identity on {platform_name}.'), { - platform_name: self.options.platformName - } - ) - })); 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() && (this.options.accountSettingsModel.get('accomplishments_shared'))) { tabs = [ {view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'}, @@ -108,7 +95,8 @@ Backbone.history.start(); } } else { - this.$el.find('.wrapper-profile-section-container-two').append(this.sectionTwoView.render().el); + // xss-lint: disable=javascript-jquery-html + this.$el.find('.wrapper-profile-bio').html(this.sectionTwoView.render().el); } return this; }, diff --git a/openedx/features/learner_profile/static/learner_profile/templates/learner_profile.underscore b/openedx/features/learner_profile/static/learner_profile/templates/learner_profile.underscore deleted file mode 100644 index 2ff1a6f397..0000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/learner_profile.underscore +++ /dev/null @@ -1,25 +0,0 @@ -
-
-
- <% if (ownProfile) { %> -
-
<%- profile_header %>
-
<%- profile_subheader %>
-
- <% } %> -
-
-
-
-
-
-
- -
-
-
-
-
diff --git a/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html b/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html new file mode 100644 index 0000000000..083bf638b2 --- /dev/null +++ b/openedx/features/learner_profile/templates/learner_profile/learner-achievements-fragment.html @@ -0,0 +1,88 @@ +## mako + +<%page expression_filter="h"/> + +<%namespace name='static' file='/static_content.html'/> + +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> + +
+ % if course_certificates or own_profile: +

Course Certificates

+ % if course_certificates: + % for certificate in course_certificates: + <% + certificate_url = certificate['download_url'] + course = certificate['course'] + + completion_date_message_html = Text(_('Completed {completion_date_html}')).format( + completion_date=HTML( + '' + ).format( + completion_date=certificate['created'], + user_timezone=user_timezone, + user_language=user_language, + ), + ) + %> + % if certificate_url: + +
+ +
+
${course.display_org_with_default}
+
${course.display_name_with_default}
+

${completion_date_message_html}

+
+
+
+ % else: +
+ +
+
${course.display_org_with_default}
+
${course.display_name_with_default}
+

${completion_date_message_html}

+
+
+ % endif + % endfor + % elif own_profile: +
+

${_("You haven't earned any certificates yet.")}

+

+ + + ${_('Explore New Courses')} + +

+
+ % endif + % endif +
+ +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform('.localized-datetime'); + + diff --git a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html b/openedx/features/learner_profile/templates/learner_profile/learner_profile.html index 02664c737d..9432128915 100644 --- a/openedx/features/learner_profile/templates/learner_profile/learner_profile.html +++ b/openedx/features/learner_profile/templates/learner_profile/learner_profile.html @@ -4,28 +4,65 @@ <%inherit file="/main.html" /> <%def name="online_help_token()"><% return "profile" %> <%namespace name='static' file='/static_content.html'/> + <%! import json from django.core.urlresolvers import reverse from django.utils.translation import ugettext 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 name="bodyclass">view-profile +<%block name="headextra"> +<%static:css group='style-course'/> + +
-
-

${_("Loading")}

+
+ +
-<%block name="headextra"> - <%static:css group='style-course'/> - <%block name="js_extra"> <%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory"> diff --git a/openedx/features/learner_profile/tests/test_views.py b/openedx/features/learner_profile/tests/views/test_learner_profile.py similarity index 51% rename from openedx/features/learner_profile/tests/test_views.py rename to openedx/features/learner_profile/tests/views/test_learner_profile.py index b5468acd11..07789d8fa6 100644 --- a/openedx/features/learner_profile/tests/test_views.py +++ b/openedx/features/learner_profile/tests/views/test_learner_profile.py @@ -1,21 +1,32 @@ # -*- coding: utf-8 -*- """ Tests for student profile views. """ +import datetime +import ddt + from django.conf import settings from django.core.urlresolvers import reverse -from django.test import TestCase from django.test.client import RequestFactory -from student.tests.factories import UserFactory from util.testing import UrlResetMixin -from ..views import learner_profile_context +from course_modes.models import CourseMode + +from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error +from student.tests.factories import CourseEnrollmentFactory, UserFactory + +from openedx.features.learner_profile.views.learner_profile import learner_profile_context +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory -class LearnerProfileViewTest(UrlResetMixin, TestCase): +@ddt.ddt +class LearnerProfileViewTest(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', @@ -32,7 +43,13 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase): def setUp(self): super(LearnerProfileViewTest, self).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): """ @@ -100,3 +117,48 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase): profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"}) response = self.client.get(path=profile_path) self.assertEqual(404, response.status_code) + + def _create_certificate(self, enrollment_mode): + """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=self.course.id, + mode=enrollment_mode, + download_url=self.DOWNLOAD_URL, + status="downloadable" + ) + + @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(cert_mode) + cert.save() + + request = RequestFactory().get('/url') + request.user = self.user + context = learner_profile_context(request, self.user.username, self.user.is_staff) + + self.assertTrue('card certificate-card mode-' + cert_mode in str(context['achievements_fragment'].content)) + + @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. + """ + request = RequestFactory().get('/url') + request.user = self.user + profile_username = self.user.username if own_profile else self.other_user.username + context = learner_profile_context(request, profile_username, self.user.is_staff) + + if own_profile: + content = str(context['achievements_fragment'].content) + self.assertIn('icon fa fa-search', content) + self.assertIn("You haven't earned any certificates yet", content) + else: + self.assertIsNone(context['achievements_fragment']) diff --git a/openedx/features/learner_profile/urls.py b/openedx/features/learner_profile/urls.py index 184feb165b..775d6ded1f 100644 --- a/openedx/features/learner_profile/urls.py +++ b/openedx/features/learner_profile/urls.py @@ -5,12 +5,19 @@ Defines URLs for the learner profile. from django.conf import settings from django.conf.urls import url +from views.learner_achievements import LearnerAchievementsFragmentView + urlpatterns = [ url( r'^{username_pattern}$'.format( username_pattern=settings.USERNAME_PATTERN, ), - 'openedx.features.learner_profile.views.learner_profile', + 'openedx.features.learner_profile.views.learner_profile.learner_profile', name='learner_profile', ), + url( + r'^achievements$', + LearnerAchievementsFragmentView.as_view(), + name='openedx.learner_profile.learner_achievements_fragment_view', + ), ] diff --git a/openedx/features/learner_profile/views/__init__.py b/openedx/features/learner_profile/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/learner_profile/views/learner_achievements.py b/openedx/features/learner_profile/views/learner_achievements.py new file mode 100644 index 0000000000..f605e192df --- /dev/null +++ b/openedx/features/learner_profile/views/learner_achievements.py @@ -0,0 +1,42 @@ +""" +Views to render a learner's achievements. +""" + +from courseware.courses import get_course_overview_with_access +from django.template.loader import render_to_string +from lms.djangoapps.certificates import api as certificate_api +from openedx.core.djangoapps.plugin_api.views import EdxFragmentView +from web_fragments.fragment import Fragment + + +class LearnerAchievementsFragmentView(EdxFragmentView): + """ + A fragment to render a learner's achievements. + """ + def render_to_fragment(self, request, username=None, own_profile=False, **kwargs): + """ + 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) + for course_certificate in course_certificates: + course_key = course_certificate['course_key'] + course_overview = get_course_overview_with_access(request.user, 'load', course_key) + course_certificate['course'] = course_overview + course_certificates.sort(key=lambda certificate: certificate['course'].display_name_with_default) + return course_certificates diff --git a/openedx/features/learner_profile/views.py b/openedx/features/learner_profile/views/learner_profile.py similarity index 88% rename from openedx/features/learner_profile/views.py rename to openedx/features/learner_profile/views/learner_profile.py index a8d377bf78..4a166d4bc8 100644 --- a/openedx/features/learner_profile/views.py +++ b/openedx/features/learner_profile/views/learner_profile.py @@ -17,6 +17,10 @@ from openedx.core.djangoapps.user_api.errors import UserNotAuthorized, UserNotFo from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences from student.models import User +from .. import SHOW_ACHIEVEMENTS_FLAG + +from learner_achievements import LearnerAchievementsFragmentView + @login_required @require_http_methods(['GET']) @@ -70,7 +74,19 @@ def learner_profile_context(request, profile_username, user_is_staff): preferences_data = get_user_preferences(profile_user, profile_username) + if SHOW_ACHIEVEMENTS_FLAG.is_enabled(): + achievements_fragment = LearnerAchievementsFragmentView().render_to_fragment( + request, + username=profile_user.username, + own_profile=own_profile, + ) + else: + achievements_fragment = None + context = { + 'own_profile': own_profile, + 'achievements_fragment': achievements_fragment, + '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'], diff --git a/themes/edge.edx.org/lms/static/images/certificates/honor.png b/themes/edge.edx.org/lms/static/images/certificates/honor.png new file mode 100644 index 0000000000..7c6878175d Binary files /dev/null and b/themes/edge.edx.org/lms/static/images/certificates/honor.png differ diff --git a/themes/edge.edx.org/lms/static/images/certificates/micromasters-program.png b/themes/edge.edx.org/lms/static/images/certificates/micromasters-program.png new file mode 100644 index 0000000000..fa1f4c824a Binary files /dev/null and b/themes/edge.edx.org/lms/static/images/certificates/micromasters-program.png differ diff --git a/themes/edge.edx.org/lms/static/images/certificates/professional-program.png b/themes/edge.edx.org/lms/static/images/certificates/professional-program.png new file mode 100644 index 0000000000..59a2e6889f Binary files /dev/null and b/themes/edge.edx.org/lms/static/images/certificates/professional-program.png differ diff --git a/themes/edge.edx.org/lms/static/images/certificates/professional.png b/themes/edge.edx.org/lms/static/images/certificates/professional.png new file mode 100644 index 0000000000..c475ca08f6 Binary files /dev/null and b/themes/edge.edx.org/lms/static/images/certificates/professional.png differ diff --git a/themes/edge.edx.org/lms/static/images/certificates/verified.png b/themes/edge.edx.org/lms/static/images/certificates/verified.png new file mode 100644 index 0000000000..0ea8365f51 Binary files /dev/null and b/themes/edge.edx.org/lms/static/images/certificates/verified.png differ diff --git a/themes/edge.edx.org/lms/static/sass/partials/base/_certificates.scss b/themes/edge.edx.org/lms/static/sass/partials/base/_certificates.scss new file mode 100644 index 0000000000..6fbee80f76 --- /dev/null +++ b/themes/edge.edx.org/lms/static/sass/partials/base/_certificates.scss @@ -0,0 +1,30 @@ +// Certificate overrides for edge.edx.org + +.certificate-card { + // Note: edx.org no longer supports audit certificates, but there are + // legacy certificates that might be rendered. In this situation, they + // are styled as honor certificates. + &.mode-honor, &.mode-audit { + border-color: $honor-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/honor.png'); + } + } + + &.mode-verified { + border-color: $verified-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/verified.png'); + } + } + + &.mode-professional { + border-color: $professional-certificate-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/professional.png'); + } + } +} diff --git a/themes/edge.edx.org/lms/static/sass/partials/base/_theme.scss b/themes/edge.edx.org/lms/static/sass/partials/base/_theme.scss new file mode 100644 index 0000000000..9be47d0644 --- /dev/null +++ b/themes/edge.edx.org/lms/static/sass/partials/base/_theme.scss @@ -0,0 +1,3 @@ +// Theme overrides for edge.edx.org + +@import 'base/certificates'; diff --git a/themes/edx.org/lms/static/images/certificates/honor.png b/themes/edx.org/lms/static/images/certificates/honor.png new file mode 100644 index 0000000000..3005543f96 Binary files /dev/null and b/themes/edx.org/lms/static/images/certificates/honor.png differ diff --git a/themes/edx.org/lms/static/images/certificates/micromasters-program.png b/themes/edx.org/lms/static/images/certificates/micromasters-program.png new file mode 100644 index 0000000000..99a20664a6 Binary files /dev/null and b/themes/edx.org/lms/static/images/certificates/micromasters-program.png differ diff --git a/themes/edx.org/lms/static/images/certificates/professional-program.png b/themes/edx.org/lms/static/images/certificates/professional-program.png new file mode 100644 index 0000000000..65dfbeee12 Binary files /dev/null and b/themes/edx.org/lms/static/images/certificates/professional-program.png differ diff --git a/themes/edx.org/lms/static/images/certificates/professional.png b/themes/edx.org/lms/static/images/certificates/professional.png new file mode 100644 index 0000000000..77cb023476 Binary files /dev/null and b/themes/edx.org/lms/static/images/certificates/professional.png differ diff --git a/themes/edx.org/lms/static/images/certificates/verified.png b/themes/edx.org/lms/static/images/certificates/verified.png new file mode 100644 index 0000000000..31787c77d0 Binary files /dev/null and b/themes/edx.org/lms/static/images/certificates/verified.png differ diff --git a/themes/edx.org/lms/static/sass/partials/base/_certificates.scss b/themes/edx.org/lms/static/sass/partials/base/_certificates.scss new file mode 100644 index 0000000000..8d3c2e1f1d --- /dev/null +++ b/themes/edx.org/lms/static/sass/partials/base/_certificates.scss @@ -0,0 +1,30 @@ +// Certificate overrides for edx.org + +.certificate-card { + // Note: edx.org no longer supports audit certificates, but there are + // legacy certificates that might be rendered. In this situation, they + // are styled as honor certificates. + &.mode-honor, &.mode-audit { + border-color: $honor-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/honor.png'); + } + } + + &.mode-verified { + border-color: $verified-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/verified.png'); + } + } + + &.mode-professional { + border-color: $professional-certificate-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/professional.png'); + } + } +} diff --git a/themes/edx.org/lms/static/sass/partials/base/_theme.scss b/themes/edx.org/lms/static/sass/partials/base/_theme.scss new file mode 100644 index 0000000000..603c4f5244 --- /dev/null +++ b/themes/edx.org/lms/static/sass/partials/base/_theme.scss @@ -0,0 +1,3 @@ +// Theme overrides for edx.org + +@import 'base/certificates'; diff --git a/themes/red-theme/lms/static/images/certificates/red-certificate.png b/themes/red-theme/lms/static/images/certificates/red-certificate.png new file mode 100644 index 0000000000..ee8f9bc053 Binary files /dev/null and b/themes/red-theme/lms/static/images/certificates/red-certificate.png differ diff --git a/themes/red-theme/lms/static/sass/partials/base/_certificates.scss b/themes/red-theme/lms/static/sass/partials/base/_certificates.scss new file mode 100644 index 0000000000..a0ebf3f820 --- /dev/null +++ b/themes/red-theme/lms/static/sass/partials/base/_certificates.scss @@ -0,0 +1,35 @@ +// Certificate overrides for the red theme + +.certificate-card { + &.mode-audit { + border-color: $audit-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/red-certificate.png'); + } + } + + &.mode-honor { + border-color: $honor-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/red-certificate.png'); + } + } + + &.mode-verified { + border-color: $verified-mode-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/red-certificate.png'); + } + } + + &.mode-professional { + border-color: $professional-certificate-color; + + .card-logo { + background-image: url('#{$static-path}/images/certificates/red-certificate.png'); + } + } +} diff --git a/themes/red-theme/lms/static/sass/partials/base/_theme.scss b/themes/red-theme/lms/static/sass/partials/base/_theme.scss new file mode 100644 index 0000000000..ab18a6a002 --- /dev/null +++ b/themes/red-theme/lms/static/sass/partials/base/_theme.scss @@ -0,0 +1,3 @@ +// Theme overrides for the red theme + +@import 'base/certificates';