Merge pull request #15868 from edx/andya/profile-certificates

LEARNER-1860: Course certificates on the learner profile
This commit is contained in:
Andy Armstrong
2017-09-01 07:12:03 -04:00
committed by GitHub
47 changed files with 685 additions and 217 deletions

3
.gitignore vendored
View File

@@ -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/

View File

@@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -7,3 +7,4 @@
@import 'base/reset';
@import 'base/variables';
@import 'base/mixins';
@import 'base/theme';

View File

@@ -6,3 +6,4 @@
@import 'base/reset';
@import 'base/variables';
@import 'base/mixins';
@import 'base/theme';

View File

@@ -4,6 +4,7 @@
// base - utilities
@import 'base/variables';
@import 'base/mixins';
@import 'base/theme';
footer#footer-edx-v3 {
@import 'base/extends';

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -7,4 +7,4 @@
@import 'base/variables-rtl';
// Import shared build for the edx.org footer
@import 'build-footer-edx'
@import 'build-footer-edx';

View File

@@ -7,4 +7,4 @@
@import 'base/variables-ltr';
// Import shared build for the edx.org footer
@import 'build-footer-edx'
@import 'build-footer-edx';

View File

@@ -8,6 +8,7 @@
// base - utilities
@import 'base/variables';
@import 'base/mixins';
@import 'base/theme';
footer#footer-openedx {
@import 'base/reset';

View File

@@ -8,6 +8,7 @@
// base - utilities
@import 'base/variables';
@import 'base/mixins';
@import 'base/theme';
footer#footer-openedx {
@import 'base/reset';

View File

@@ -0,0 +1 @@
// File to be overridden by themes

View File

@@ -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.

View File

@@ -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')

View File

@@ -1,19 +1,40 @@
<div class="message-banner" aria-live="polite"></div>
<div class="wrapper-profile">
<div class="ui-loading-indicator">
<p>
<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">
<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">
</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>
</div>

View File

@@ -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.'

View File

@@ -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;
},

View File

@@ -1,25 +0,0 @@
<div class="profile <%- ownProfile ? 'profile-self' : 'profile-other' %>">
<div class="wrapper-profile-field-account-privacy"></div>
<div class="wrapper-profile-sections account-settings-container">
<% if (ownProfile) { %>
<div class="profile-header">
<div class="header"> <%- profile_header %></div>
<div class="subheader"> <%- profile_subheader %></div>
</div>
<% } %>
<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"><%- gettext("An error occurred. Try loading the page again.") %></span>
</div>
</div>
<div class="wrapper-profile-section-container-two">
</div>
</div>
</div>

View File

@@ -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
%>
<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:
<%
certificate_url = certificate['download_url']
course = certificate['course']
completion_date_message_html = Text(_('Completed {completion_date_html}')).format(
completion_date=HTML(
'<span'
' class="localized-datetime start-date"'
' data-datetime="{completion_date_html}"'
' 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,
),
)
%>
% if certificate_url:
<a href="${certificate_url}" target="_blank">
<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>
</a>
% else:
<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>
% endif
% endfor
% elif own_profile:
<div class="learner-message">
<h4 class="message-header">${_("You haven't earned any certificates yet.")}</h4>
<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>
</div>
% endif
% endif
</div>
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
DateUtilFactory.transform('.localized-datetime');
</%static:require_module_async>

View File

@@ -4,28 +4,65 @@
<%inherit file="/main.html" />
<%def name="online_help_token()"><% return "profile" %></%def>
<%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>
<%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="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 class="profile ${'profile-self' if own_profile else 'profile-other'}">
<div class="wrapper-profile-field-account-privacy"></div>
<div class="wrapper-profile-sections account-settings-container">
% if own_profile:
<div class="profile-header">
<div class="header">${_("My Profile")}</div>
<div class="subheader">
${_('Build out your profile to personalize your identity on {platform_name}.').format(
platform_name=platform_name,
)}
</div>
</div>
% endif
<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">
% if achievements_fragment:
${HTML(achievements_fragment.body_html())}
% endif
<div class="wrapper-profile-bio">
</div>
</div>
</div>
</div>
</div>
</main>
<%block name="headextra">
<%static:css group='style-course'/>
</%block>
<%block name="js_extra">
<%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory">

View File

@@ -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'])

View File

@@ -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',
),
]

View File

@@ -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

View File

@@ -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'],

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -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');
}
}
}

View File

@@ -0,0 +1,3 @@
// Theme overrides for edge.edx.org
@import 'base/certificates';

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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');
}
}
}

View File

@@ -0,0 +1,3 @@
// Theme overrides for edx.org
@import 'base/certificates';

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -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');
}
}
}

View File

@@ -0,0 +1,3 @@
// Theme overrides for the red theme
@import 'base/certificates';