Commit 6f888d69 by Andy Armstrong

Add course certificates on the learner profile

LEARNER-1860
parent b31287e2
...@@ -107,8 +107,7 @@ cms/static/css/ ...@@ -107,8 +107,7 @@ cms/static/css/
cms/static/sass/*.css cms/static/sass/*.css
cms/static/sass/*.css.map cms/static/sass/*.css.map
cms/static/themed_sass/ cms/static/themed_sass/
themes/**/css/*.css themes/**/css
themes/**/css/discussion/*.css
### Logging artifacts ### Logging artifacts
log/ log/
......
...@@ -245,6 +245,7 @@ def cert_info(user, course_overview, course_mode): ...@@ -245,6 +245,7 @@ def cert_info(user, course_overview, course_mode):
""" """
if not course_overview.may_certify(): if not course_overview.may_certify():
return {} return {}
# Note: this should be rewritten to use the certificates API
return _cert_info( return _cert_info(
user, user,
course_overview, course_overview,
......
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
@import 'base/reset'; @import 'base/reset';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
...@@ -6,3 +6,4 @@ ...@@ -6,3 +6,4 @@
@import 'base/reset'; @import 'base/reset';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
// base - utilities // base - utilities
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
footer#footer-edx-v3 { footer#footer-edx-v3 {
@import 'base/extends'; @import 'base/extends';
......
...@@ -5,5 +5,6 @@ ...@@ -5,5 +5,6 @@
@import 'base/variables'; @import 'base/variables';
@import 'base/font_face'; @import 'base/font_face';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
@import 'build-course'; // shared app style assets/rendering @import 'build-course'; // shared app style assets/rendering
...@@ -5,5 +5,6 @@ ...@@ -5,5 +5,6 @@
@import 'base/variables'; @import 'base/variables';
@import 'base/font_face'; @import 'base/font_face';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
@import 'build-course'; // shared app style assets/rendering @import 'build-course'; // shared app style assets/rendering
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
@import 'base/variables-rtl'; @import 'base/variables-rtl';
// Import shared build for the edx.org footer // Import shared build for the edx.org footer
@import 'build-footer-edx' @import 'build-footer-edx';
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
@import 'base/variables-ltr'; @import 'base/variables-ltr';
// Import shared build for the edx.org footer // Import shared build for the edx.org footer
@import 'build-footer-edx' @import 'build-footer-edx';
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
// base - utilities // base - utilities
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
footer#footer-openedx { footer#footer-openedx {
@import 'base/reset'; @import 'base/reset';
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
// base - utilities // base - utilities
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins'; @import 'base/mixins';
@import 'base/theme';
footer#footer-openedx { footer#footer-openedx {
@import 'base/reset'; @import 'base/reset';
......
// File to be overridden by themes
...@@ -248,11 +248,14 @@ $state-danger-border: darken($state-danger-bg, 5%) !default; ...@@ -248,11 +248,14 @@ $state-danger-border: darken($state-danger-bg, 5%) !default;
// ---------------------------- // ----------------------------
// logo colors // logo colors
$micromasters-color: #005585; $audit-mode-color: $gray-dark !default;
$xseries-color: #424242; $honor-mode-color: $uxpl-blue-base !default;
$professional-certificate-color: #9a1f60; $verified-mode-color: $uxpl-green-base !default;
$zebra-stripe-color: rgb(249, 250, 252); $micromasters-color: #005585 !default;
$divider-color: rgb(226,231,236); $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 // old color variables
// DEPRECATED: Do not continue to use these colors, instead use pattern libary and base colors above. // DEPRECATED: Do not continue to use these colors, instead use pattern libary and base colors above.
......
"""
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')
<div class="message-banner" aria-live="polite"></div> <div class="message-banner" aria-live="polite"></div>
<div class="wrapper-profile"> <div class="wrapper-profile">
<div class="ui-loading-indicator"> <div class="profile profile-other">
<p> <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="spin">
<span class="icon fa fa-refresh" aria-hidden="true"></span> <span class="icon fa fa-refresh" aria-hidden="true"></span>
</span> </span>
<span class="copy"> <span class="copy">
Loading Loading
</span> </span>
</p> </p>
</div> </div>
<div class="ui-loading-error is-hidden"> <div class="ui-loading-error is-hidden">
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span> <span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
<span class="copy"> <span class="copy">
An error occurred. Please reload the page. An error occurred. Please reload the page.
</span> </span>
</div> </div>
</div> </div>
...@@ -36,14 +36,14 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' ...@@ -36,14 +36,14 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
}; };
var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) { var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) {
var accountPrivacyElement = learnerProfileView.$('.wrapper-profile-field-account-privacy'); var $accountPrivacyElement = $('.wrapper-profile-field-account-privacy');
var privacyFieldElement = $(accountPrivacyElement).find('.u-field'); var $privacyFieldElement = $($accountPrivacyElement).find('.u-field');
if (othersProfile) { if (othersProfile) {
expect(privacyFieldElement.length).toBe(0); expect($privacyFieldElement.length).toBe(0);
} else { } else {
expect(privacyFieldElement.length).toBe(1); expect($privacyFieldElement.length).toBe(1);
expectProfileElementContainsField(privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView); expectProfileElementContainsField($privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView);
} }
}; };
...@@ -65,12 +65,12 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' ...@@ -65,12 +65,12 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
}; };
var expectSectionTwoTobeRendered = function(learnerProfileView) { var expectSectionTwoTobeRendered = function(learnerProfileView) {
var sectionTwoElement = learnerProfileView.$('.wrapper-profile-section-two'); var $sectionTwoElement = $('.wrapper-profile-section-two');
var sectionTwoFieldElements = $(sectionTwoElement).find('.u-field'); 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( expectProfileElementContainsField(
sectionFieldElement, sectionFieldElement,
learnerProfileView.options.sectionTwoFieldViews[fieldIndex] learnerProfileView.options.sectionTwoFieldViews[fieldIndex]
...@@ -85,7 +85,7 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' ...@@ -85,7 +85,7 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
}; };
var expectLimitedProfileSectionsAndFieldsToBeRendered = function(learnerProfileView, othersProfile) { 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); expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
...@@ -108,9 +108,9 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' ...@@ -108,9 +108,9 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
}; };
var expectProfileSectionsNotToBeRendered = function(learnerProfileView) { var expectProfileSectionsNotToBeRendered = function(learnerProfileView) {
expect(learnerProfileView.$('.wrapper-profile-field-account-privacy').length).toBe(0); expect($('.wrapper-profile-field-account-privacy').length).toBe(0);
expect(learnerProfileView.$('.wrapper-profile-section-one').length).toBe(0); expect($('.wrapper-profile-section-one').length).toBe(0);
expect(learnerProfileView.$('.wrapper-profile-section-two').length).toBe(0); expect($('.wrapper-profile-section-two').length).toBe(0);
}; };
var expectTabbedViewToBeUndefined = function(requests, tabbedViewView) { var expectTabbedViewToBeUndefined = function(requests, tabbedViewView) {
...@@ -124,42 +124,42 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' ...@@ -124,42 +124,42 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
}; };
var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) { var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) {
var badgeListingView = learnerProfileView.$el.find('#tabpanel-accomplishments'), var $badgeListingView = $('#tabpanel-accomplishments'),
updatedLength = length, updatedLength = length,
placeholder; placeholder;
expect(learnerProfileView.$el.find('#tabpanel-about_me').hasClass('is-hidden')).toBe(true); expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(true);
expect(badgeListingView.hasClass('is-hidden')).toBe(false); expect($badgeListingView.hasClass('is-hidden')).toBe(false);
if (lastPage) { if (lastPage) {
updatedLength += 1; updatedLength += 1;
placeholder = badgeListingView.find('.find-course'); placeholder = $badgeListingView.find('.find-course');
expect(placeholder.length).toBe(1); expect(placeholder.length).toBe(1);
expect(placeholder.attr('href')).toBe('/courses/'); 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 expectBadgesHidden = function(learnerProfileView) {
var accomplishmentsTab = learnerProfileView.$el.find('#tabpanel-accomplishments'); var $accomplishmentsTab = $('#tabpanel-accomplishments');
if (accomplishmentsTab.length) { if ($accomplishmentsTab.length) {
// Nonexistence counts as hidden. // 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 expectPage = function(learnerProfileView, pageData) {
var badgeListContainer = learnerProfileView.$el.find('#tabpanel-accomplishments'); var $badgeListContainer = $('#tabpanel-accomplishments');
var index = badgeListContainer.find('span.search-count').text().trim(); var index = $badgeListContainer.find('span.search-count').text().trim();
expect(index).toBe('Showing ' + (pageData.start + 1) + '-' + (pageData.start + pageData.results.length) + expect(index).toBe('Showing ' + (pageData.start + 1) + '-' + (pageData.start + pageData.results.length) +
' out of ' + pageData.count + ' total'); ' 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) { _.each(pageData.results, function(badge) {
expect($('.badge-display:contains(' + badge.badge_class.display_name + ')').length).toBe(1); expect($('.badge-display:contains(' + badge.badge_class.display_name + ')').length).toBe(1);
}); });
}; };
var expectBadgeLoadingErrorIsRendered = function(learnerProfileView) { var expectBadgeLoadingErrorIsRendered = function(learnerProfileView) {
var errorMessage = learnerProfileView.$el.find('.badge-set-display').text(); var errorMessage = $('.badge-set-display').text();
expect(errorMessage).toBe( expect(errorMessage).toBe(
'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' + 'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' +
'Help tab to report the problem.' 'Help tab to report the problem.'
......
...@@ -5,11 +5,9 @@ ...@@ -5,11 +5,9 @@
[ [
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils', 'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/tabbed_view', 'common/js/components/views/tabbed_view',
'learner_profile/js/views/section_two_tab', 'learner_profile/js/views/section_two_tab'
'text!learner_profile/templates/learner_profile.underscore',
'edx-ui-toolkit/js/utils/string-utils'
], ],
function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab, learnerProfileTemplate, StringUtils) { function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab) {
var LearnerProfileView = Backbone.View.extend({ var LearnerProfileView = Backbone.View.extend({
initialize: function(options) { initialize: function(options) {
...@@ -25,8 +23,6 @@ ...@@ -25,8 +23,6 @@
this.firstRender = true; this.firstRender = true;
}, },
template: _.template(learnerProfileTemplate),
showFullProfile: function() { showFullProfile: function() {
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge(); var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
if (this.options.ownProfile) { if (this.options.ownProfile) {
...@@ -54,22 +50,13 @@ ...@@ -54,22 +50,13 @@
ownProfile: this.options.ownProfile 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(); 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'))) { if (this.showFullProfile() && (this.options.accountSettingsModel.get('accomplishments_shared'))) {
tabs = [ tabs = [
{view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'}, {view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'},
...@@ -108,7 +95,8 @@ ...@@ -108,7 +95,8 @@
Backbone.history.start(); Backbone.history.start();
} }
} else { } 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; return this;
}, },
......
<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>
## 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>
...@@ -4,28 +4,65 @@ ...@@ -4,28 +4,65 @@
<%inherit file="/main.html" /> <%inherit file="/main.html" />
<%def name="online_help_token()"><% return "profile" %></%def> <%def name="online_help_token()"><% return "profile" %></%def>
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%! <%!
import json import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import dump_js_escaped_json 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="pagetitle">${_("Learner Profile")}</%block>
<%block name="bodyclass">view-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> <div class="message-banner" aria-live="polite"></div>
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
<div class="wrapper-profile"> <div class="wrapper-profile">
<div class="ui-loading-indicator"> <div class="profile ${'profile-self' if own_profile else 'profile-other'}">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p> <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>
</div> </div>
</main> </main>
<%block name="headextra">
<%static:css group='style-course'/>
</%block>
<%block name="js_extra"> <%block name="js_extra">
<%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory"> <%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory">
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Tests for student profile views. """ """ Tests for student profile views. """
import datetime
import ddt
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from student.tests.factories import UserFactory
from util.testing import UrlResetMixin 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. """ """ Tests for the student profile view. """
USERNAME = "username" USERNAME = "username"
OTHER_USERNAME = "other_user"
PASSWORD = "password" PASSWORD = "password"
DOWNLOAD_URL = "http://www.example.com/certificate.pdf"
CONTEXT_DATA = [ CONTEXT_DATA = [
'default_public_account_fields', 'default_public_account_fields',
'accounts_api_url', 'accounts_api_url',
...@@ -32,7 +43,13 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase): ...@@ -32,7 +43,13 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase):
def setUp(self): def setUp(self):
super(LearnerProfileViewTest, self).setUp() super(LearnerProfileViewTest, self).setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) 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.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): def test_context(self):
""" """
...@@ -100,3 +117,48 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase): ...@@ -100,3 +117,48 @@ class LearnerProfileViewTest(UrlResetMixin, TestCase):
profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"}) profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"})
response = self.client.get(path=profile_path) response = self.client.get(path=profile_path)
self.assertEqual(404, response.status_code) 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'])
...@@ -5,12 +5,19 @@ Defines URLs for the learner profile. ...@@ -5,12 +5,19 @@ Defines URLs for the learner profile.
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from views.learner_achievements import LearnerAchievementsFragmentView
urlpatterns = [ urlpatterns = [
url( url(
r'^{username_pattern}$'.format( r'^{username_pattern}$'.format(
username_pattern=settings.USERNAME_PATTERN, username_pattern=settings.USERNAME_PATTERN,
), ),
'openedx.features.learner_profile.views.learner_profile', 'openedx.features.learner_profile.views.learner_profile.learner_profile',
name='learner_profile', name='learner_profile',
), ),
url(
r'^achievements$',
LearnerAchievementsFragmentView.as_view(),
name='openedx.learner_profile.learner_achievements_fragment_view',
),
] ]
"""
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
...@@ -17,6 +17,10 @@ from openedx.core.djangoapps.user_api.errors import UserNotAuthorized, UserNotFo ...@@ -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 openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
from student.models import User from student.models import User
from .. import SHOW_ACHIEVEMENTS_FLAG
from learner_achievements import LearnerAchievementsFragmentView
@login_required @login_required
@require_http_methods(['GET']) @require_http_methods(['GET'])
...@@ -70,7 +74,19 @@ def learner_profile_context(request, profile_username, user_is_staff): ...@@ -70,7 +74,19 @@ def learner_profile_context(request, profile_username, user_is_staff):
preferences_data = get_user_preferences(profile_user, profile_username) 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 = { context = {
'own_profile': own_profile,
'achievements_fragment': achievements_fragment,
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
'data': { 'data': {
'profile_user_id': profile_user.id, 'profile_user_id': profile_user.id,
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'], 'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
......
// 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');
}
}
}
// Theme overrides for edge.edx.org
@import 'base/certificates';
// 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');
}
}
}
// Theme overrides for edx.org
@import 'base/certificates';
// 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');
}
}
}
// Theme overrides for the red theme
@import 'base/certificates';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment