Commit 8196e1a0 by Renzo Lucioni

Allow program listing page to display programs from any category

This work removes most references to XSeries from the LMS in an attempt to be more general. ECOM-5018.
parent 92f3c7ee
......@@ -889,7 +889,7 @@ class AnonymousLookupTable(ModuleStoreTestCase):
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
# TODO: Clean up these tests so that they use program factories.
# TODO: Clean up these tests so that they use program factories and don't mention XSeries!
@attr('shard_3')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
......@@ -907,8 +907,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
self.course_2 = CourseFactory.create()
self.course_3 = CourseFactory.create()
self.program_name = 'Testing Program'
self.category = 'xseries'
self.display_category = 'XSeries'
self.category = 'XSeries'
CourseModeFactory.create(
course_id=self.course_1.id,
......@@ -990,8 +989,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
self.assertEqual(
{
u'edx/demox/Run_1': {
'category': 'xseries',
'display_category': 'XSeries',
'category': self.category,
'course_program_list': [{
'program_id': 0,
'course_count': len(course_codes),
......@@ -1150,11 +1148,17 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
"""
self.assertContains(response, 'label-xseries-association', count)
self.assertContains(response, 'btn xseries-', count)
self.assertContains(response, 'XSeries Program Course', count)
self.assertContains(response, 'XSeries Program: Interested in more courses in this subject?', count)
self.assertContains(response, '{category} Program Course'.format(category=self.category), count)
self.assertContains(
response,
'{category} Program: Interested in more courses in this subject?'.format(category=self.category),
count
)
self.assertContains(response, 'View {category} Details'.format(category=self.category), count)
self.assertContains(response, 'This course is 1 of 3 courses in the', count)
self.assertContains(response, self.program_name, count * 2)
self.assertContains(response, 'View XSeries Details', count)
class UserAttributeTests(TestCase):
......
......@@ -120,7 +120,7 @@ from notification_prefs.views import enable_notifications
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard, get_display_category
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_helpers
......@@ -2497,7 +2497,7 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
for course_key, programs in course_programs.viewitems():
for program in programs:
if program.get('status') == 'active' and program.get('category') == 'xseries':
if program.get('status') == 'active' and program.get('category') == 'XSeries':
try:
programs_for_course = programs_data.setdefault(course_key, {})
programs_for_course.setdefault('course_program_list', []).append({
......@@ -2510,7 +2510,6 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
).format(program['marketing_slug'])
})
programs_for_course['category'] = program.get('category')
programs_for_course['display_category'] = get_display_category(program)
except KeyError:
log.warning('Program structure is invalid, skipping display: %r', program)
......
......@@ -42,7 +42,6 @@ class ProgramsConfigMixin(object):
'enable_student_dashboard': is_enabled,
'enable_studio_tab': is_enabled,
'enable_certification': is_enabled,
'xseries_ad_enabled': is_enabled,
'program_listing_enabled': is_enabled,
'program_details_enabled': is_enabled,
}).install()
......@@ -23,7 +23,6 @@ from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfi
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests import factories as programs_factories
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.programs.utils import get_display_category
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
......@@ -65,8 +64,6 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
cls.data = sorted([cls.first_program, cls.second_program], key=cls.program_sort_key)
cls.marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').rstrip('/')
def setUp(self):
super(TestProgramListing, self).setUp()
......@@ -187,30 +184,19 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
for index, actual_program in enumerate(actual):
expected_program = self.data[index]
self.assert_dict_contains_subset(actual_program, expected_program)
self.assertEqual(
actual_program['display_category'],
get_display_category(expected_program)
)
def test_toggle_xseries_advertising(self):
def test_program_discovery(self):
"""
Verify that when XSeries advertising is disabled, no link to the marketing site
appears in the response (and vice versa).
Verify that a link to a programs marketing page appears in the response.
"""
# Verify the URL is present when advertising is enabled.
self.create_programs_config()
self.create_programs_config(marketing_path='bar')
self.mock_programs_api(self.data)
response = self.client.get(self.url)
self.assertContains(response, self.marketing_root)
# Verify the URL is missing when advertising is disabled.
self.create_programs_config(xseries_ad_enabled=False)
marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'bar').rstrip('/')
response = self.client.get(self.url)
self.assertNotContains(response, self.marketing_root)
self.assertContains(response, marketing_root)
def test_links_to_detail_pages(self):
"""
......@@ -237,7 +223,8 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
)
# Verify that links to the marketing site are present when detail pages are disabled.
self.create_programs_config(program_details_enabled=False)
self.create_programs_config(program_details_enabled=False, marketing_path='bar')
marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'bar').rstrip('/')
response = self.client.get(self.url)
actual = self.load_serialized_data(response, 'programsData')
......@@ -248,7 +235,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
self.assertEqual(
actual_program['detail_url'],
'{}/{}'.format(self.marketing_root, expected_program['marketing_slug'])
'{}/{}'.format(marketing_root, expected_program['marketing_slug'])
)
def test_certificates_listed(self):
......
......@@ -5,7 +5,7 @@ from . import views
urlpatterns = [
url(r'^programs/$', views.view_programs, name='program_listing_view'),
url(r'^programs/$', views.program_listing, name='program_listing_view'),
# Matches paths like 'programs/123/' and 'programs/123/foo/', but not 'programs/123/foo/bar/'.
url(r'^programs/(?P<program_id>\d+)/[\w\-]*/?$', views.program_details, name='program_details_view'),
]
......@@ -8,19 +8,16 @@ from django.http import Http404
from django.views.decorators.http import require_GET
from edxmako.shortcuts import render_to_response
from lms.djangoapps.learner_dashboard.utils import strip_course_id, FAKE_COURSE_KEY
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils
from lms.djangoapps.learner_dashboard.utils import (
FAKE_COURSE_KEY,
strip_course_id
)
@login_required
@require_GET
def view_programs(request):
"""View programs in which the user is engaged."""
def program_listing(request):
"""View a list of programs in which the user is engaged."""
programs_config = ProgramsApiConfig.current()
if not programs_config.show_program_listing:
raise Http404
......@@ -28,22 +25,20 @@ def view_programs(request):
meter = utils.ProgramProgressMeter(request.user)
programs = meter.engaged_programs
# TODO: Pull 'xseries' string from configuration model.
marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').rstrip('/')
marketing_url = urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
for program in programs:
program['detail_url'] = utils.get_program_detail_url(program, marketing_root)
program['display_category'] = utils.get_display_category(program)
program['detail_url'] = utils.get_program_detail_url(program, marketing_url)
context = {
'credentials': get_programs_credentials(request.user),
'disable_courseware_js': True,
'marketing_url': marketing_url,
'nav_hidden': True,
'programs': programs,
'progress': meter.progress,
'xseries_url': marketing_root if programs_config.show_xseries_ad else None,
'nav_hidden': True,
'show_program_listing': programs_config.show_program_listing,
'credentials': get_programs_credentials(request.user, category='xseries'),
'disable_courseware_js': True,
'uses_pattern_library': True
'uses_pattern_library': True,
}
return render_to_response('learner_dashboard/programs.html', context)
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 133.28 119.8">
<path class="cls-1" d="M27.11,43.21A129,129,0,0,0,40,46.42,59.6,59.6,0,0,0,32.75,66l3.94,0.71A55.79,55.79,0,0,1,44.14,47.2,184,184,0,0,0,74.8,49.87V66.36h4V49.85a185.1,185.1,0,0,0,29.8-2.93,55.69,55.69,0,0,1,7.5,19.59l3.94-.71a59.37,59.37,0,0,0-7.28-19.68q6-1.24,12.27-3a61.23,61.23,0,0,1,11.55,23.39l3.88-1a66.47,66.47,0,0,0-128.74.08l3.88,1A61.24,61.24,0,0,1,27.11,43.21Zm19.46,0.35C56.08,30.42,69.23,23.49,74.8,21V45.89A180.18,180.18,0,0,1,46.57,43.56Zm32.23,2.3V21.35c5.93,2.72,18.28,9.49,27.35,22A181.27,181.27,0,0,1,78.8,45.86Zm43.36-6.07q-5.93,1.59-11.67,2.71a73.59,73.59,0,0,0-24.61-22A62.34,62.34,0,0,1,122.15,39.79ZM67.24,20.36c-7.22,4.05-17.4,11.23-25,22.44-5.51-1.05-9.7-2.15-12.28-2.9A62.33,62.33,0,0,1,67.24,20.36Z" transform="translate(-9.84 -15.73)"/>
<rect class="cls-1" x="9.85" y="131.42" width="133.27" height="4" transform="translate(-10.04 -15.62) rotate(-0.08)"/>
<rect class="cls-1" x="9.85" y="75.42" width="133.27" height="4" transform="translate(-9.96 -15.62) rotate(-0.08)"/>
<polygon class="cls-1" points="59.7 104.96 59.7 70.71 29.96 100.47 0.21 70.71 0.21 104.96 4.21 104.96 4.21 80.37 29.96 106.13 55.7 80.37 55.7 104.96 59.7 104.96"/>
<polygon class="cls-1" points="131.2 104.96 131.2 70.71 101.46 100.47 71.71 70.71 71.71 104.96 75.71 104.96 75.71 80.37 101.46 106.13 127.2 80.37 127.2 104.96 131.2 104.96"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.06 36.98">
<polygon class="cls-1" points="4.96 16.47 3.06 15.45 1.15 16.44 1.53 14.32 0 12.81 2.13 12.52 3.1 10.59 4.04 12.53 6.16 12.86 4.61 14.34 4.96 16.47"/>
<polygon class="cls-1" points="11.89 36.98 6.99 36.98 6.99 19.89 11.89 16.34 11.89 36.98"/>
<polygon class="cls-1" points="10.34 6.69 10.69 8.81 8.79 7.79 6.88 8.78 7.26 6.66 5.73 5.15 7.86 4.86 8.83 2.94 9.77 4.87 11.89 5.2 10.34 6.69"/>
<rect class="cls-1" x="15.59" y="12.95" width="4.89" height="24.02"/>
<polygon class="cls-1" points="19.56 3.75 19.91 5.87 18.01 4.86 16.1 5.84 16.48 3.73 14.95 2.21 17.08 1.92 18.05 0 18.99 1.94 21.11 2.26 19.56 3.75"/>
<polygon class="cls-1" points="29.06 36.98 24.17 36.98 24.18 15.99 29.06 19.6 29.06 36.98"/>
<polygon class="cls-1" points="27.23 7.79 25.31 8.78 25.7 6.66 24.17 5.15 26.3 4.86 27.26 2.94 28.2 4.87 30.33 5.2 28.77 6.69 29.12 8.81 27.23 7.79"/>
<polygon class="cls-1" points="32.95 15.45 31.04 16.44 31.42 14.32 29.89 12.81 32.02 12.52 32.99 10.59 33.93 12.53 36.06 12.86 34.51 14.35 34.85 16.47 32.95 15.45"/>
</svg>
......@@ -12,7 +12,7 @@
if (data){
this.set({
name: data.name,
type: data.display_category + ' Program',
category: data.category,
subtitle: data.subtitle,
organizations: data.organizations,
detailUrl: data.detail_url,
......
......@@ -28,10 +28,12 @@
}
}).render();
new SidebarView({
el: '.sidebar',
context: options
}).render();
if ( options.programsData.length ) {
new SidebarView({
el: '.sidebar',
context: options
}).render();
}
};
});
}).call(this, define || RequireJS.define);
......@@ -28,8 +28,8 @@
var childList;
if (!this.collection.length) {
if (this.context.xseriesUrl) {
//Only show the xseries advertising panel if the link is passed in
if (this.context.marketingUrl) {
//Only show the advertising panel if the link is passed in
HtmlUtils.setHtml(this.$el, HtmlUtils.template(emptyProgramsListTpl)(this.context));
}
} else {
......
......@@ -23,8 +23,8 @@
this.context = data.context;
this.$parentEl = $(this.parentEl);
if (this.context.xseriesUrl){
// Only render if there is an XSeries link
if (this.context.marketingUrl){
// Only render if there is a link
this.render();
} else {
/**
......
......@@ -19,7 +19,7 @@ define([
"credential_url": "https://credentials.stage.edx.org/credentials/dummy-uuid-2/"
}
],
xseriesImage: "/images/testing.png"
sampleCertImageSrc: "/images/programs/sample-cert.png"
}
};
......@@ -45,8 +45,8 @@ define([
expect($(el).html().trim()).toEqual(data.context.certificatesData[index].display_name);
expect($(el).attr('href')).toEqual(data.context.certificatesData[index].credential_url);
});
expect(view.$el.find('.hd-6').html().trim()).toEqual('XSeries Program Certificates:');
expect(view.$el.find('img').attr('src')).toEqual('/images/testing.png');
expect(view.$el.find('.hd-6').html().trim()).toEqual('Program Certificates');
expect(view.$el.find('img').attr('src')).toEqual(data.context.sampleCertImageSrc);
});
it('should display no certificate box if certificates list is empty', function() {
......
......@@ -13,8 +13,7 @@ define([
var view = null,
programModel,
program = {
category: 'xseries',
display_category: 'XSeries',
category: 'FooBar',
status: 'active',
subtitle: 'program 1',
name: 'test program 1',
......@@ -53,7 +52,7 @@ define([
cardRenders = function($card) {
expect($card).toBeDefined();
expect($card.find('.title').html().trim()).toEqual(program.name);
expect($card.find('.category span').html().trim()).toEqual('XSeries Program');
expect($card.find('.category span').html().trim()).toEqual(program.category);
expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key);
expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
};
......
......@@ -10,14 +10,14 @@ define([
describe('Sidebar View', function () {
var view = null,
context = {
xseriesUrl: 'http://www.edx.org/xseries',
marketingUrl: 'https://www.example.org/programs',
certificatesData: [
{
"display_name": "Testing",
"credential_url": "https://credentials.stage.edx.org/credentials/dummy-uuid-1/"
"credential_url": "https://credentials.example.com/credentials/uuid/"
}
],
xseriesImage: '/image/test.png'
sampleCertImageSrc: "/images/programs/sample-cert.png"
};
beforeEach(function() {
......@@ -38,18 +38,18 @@ define([
expect(view).toBeDefined();
});
it('should load the xseries advertising based on passed in xseries URL', function() {
it('should load the exploration panel given a marketing URL', function() {
var $sidebar = view.$el;
expect($sidebar.find('.program-advertise .advertise-message').html().trim())
.toEqual('Browse recently launched courses and see what\'s new in your favorite subjects');
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.xseriesUrl);
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.marketingUrl);
});
it('should load the certificates based on passed in certificates list', function() {
expect(view.$('.certificate-link').length).toBe(1);
});
it('should not load the xseries advertising if no xseriesUrl passed in', function(){
it('should not load the advertising panel if no marketing URL is provided', function(){
var $ad;
view.remove();
view = new SidebarView({
......
......@@ -58,7 +58,6 @@
@import "views/financial-assistance";
@import 'views/bookmarks';
@import 'course/auto-cert';
@import 'elements/xseries-certificates';
@import 'views/api-access';
// app - discussion
......
......@@ -6,7 +6,7 @@
overflow: hidden;
}
.program-card{
.program-card {
@include span(12);
border: 1px solid $border-color-l3;
border-bottom: none;
......@@ -21,7 +21,7 @@
}
}
.card-link{
.card-link {
@include left(0);
@include right(0);
position: absolute;
......@@ -33,11 +33,11 @@
&:active,
&:hover,
&:focus{
&:focus {
opacity: 1;
}
.banner-image-container{
.banner-image-container {
position: relative;
overflow: hidden;
height: 166px;
......@@ -46,7 +46,7 @@
@include susy-media($bp-screen-md) { height: 116px; }
@include susy-media($bp-screen-lg) { height: 145px; }
.banner-image{
.banner-image {
@include left(50%);
position: absolute;
top: 0;
......@@ -57,7 +57,7 @@
}
}
.text-section{
.text-section {
padding: 40px $baseline $baseline;
position: relative;
margin-top: 156px;
......@@ -67,7 +67,7 @@
@include susy-media($bp-screen-lg) { margin-top: 135px; }
}
.meta-info{
.meta-info {
font-size: font-size(x-small);
color: palette(grayscale, dark);
position: absolute;
......@@ -75,31 +75,39 @@
width: calc(100% - 40px);
}
.organization{
.organization {
@include span(6);
white-space: nowrap;
overflow: hidden;
}
.category{
.category {
@include span(6);
@include text-align(right);
.category-text{
@include float(right);
.category-text {
@include float(right);
}
.xseries-icon{
.category-icon {
@include float(right);
@include margin-right($baseline*0.25);
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
background-color: transparent;
background-size: 100%;
width: ($baseline*0.7);
height: ($baseline*0.7);
}
.xseries-icon{
background: url('#{$static-path}/images/programs/xseries-icon.svg') no-repeat;
}
.micromasters-icon{
margin-top: $baseline * 0.05;
background: url('#{$static-path}/images/programs/micromasters-icon.svg') no-repeat;
}
}
.hd-3 {
color: palette(grayscale, x-dark);
min-height: ($baseline*3);
......
@mixin xseries-certificate-container {
border: 1px solid $gray-l3;
box-sizing: border-box;
padding: $baseline;
background: $gray-l6;
margin-top: $baseline;
.title{
@extend %t-title6;
@extend %t-weight3;
margin-bottom:$baseline;
color: $gray;
}
.certificate-link{
padding-top: $baseline;
display: block;
}
}
......@@ -77,32 +77,4 @@
color: $black;
margin-bottom: $baseline;
}
.find-xseries-programs {
background: $black;
border-color: $black;
color: $white;
.action-xseries-icon {
@include float(left);
@include margin-right($baseline*0.4);
display: inline;
background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
&:active,
&:hover,
&:focus {
background: $white;
color: $black;
.action-xseries-icon {
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
}
}
}
}
......@@ -53,10 +53,10 @@ from student.helpers import (
<% mode_class = '' %>
% endif
<div class="course-container">
% if course_program_info and course_program_info.get('category')=='xseries':
% if course_program_info and course_program_info.get('category')=='XSeries':
<div class="label-xseries-association">
<span class="xseries-icon" aria-hidden="true"></span>
<p class="message-copy">${_("{category} Program Course").format(category=course_program_info['display_category'])}</p>
<p class="message-copy">${_("{category} Program Course").format(category=course_program_info['category'])}</p>
</div>
% endif
<article class="course${mode_class}">
......@@ -370,9 +370,9 @@ from student.helpers import (
</div>
%endif
% if course_program_info and course_program_info.get('category')=='xseries':
% if course_program_info and course_program_info.get('category'):
%for program_data in course_program_info.get('course_program_list', []):
<%include file = "_dashboard_xseries_info.html" args="program_data=program_data, enrollment_mode=enrollment.mode, display_category=course_program_info['display_category']" />
<%include file = "_dashboard_program_info.html" args="program_data=program_data, enrollment_mode=enrollment.mode, category=course_program_info['category']" />
%endfor
% endif
......
<%page expression_filter="h" args="program_data, enrollment_mode, display_category" />
<%page expression_filter="h" args="program_data, enrollment_mode, category" />
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
......@@ -8,7 +8,7 @@
<div class="xseries-action">
<div class="message-copy xseries-msg">
<p class="message-copy-bold">
${_("{category} Program: Interested in more courses in this subject?").format(category=display_category)}
${_("{category} Program: Interested in more courses in this subject?").format(category=category)}
</p>
<p class="message-copy">
${Text(_("This course is 1 of {course_count} courses in the {link_start}{program_display_name}{link_end} {program_category}.")).format(
......@@ -16,7 +16,7 @@
link_start=HTML('<a href="{}">').format(program_data['program_marketing_url']),
link_end=HTML('</a>'),
program_display_name=program_data['display_name'],
program_category=display_category,
program_category=category,
)}
</p>
......@@ -30,7 +30,7 @@
data-program-id="${program_data['program_id']}" >
<span class="sr">${program_data['display_name']}</span>
<span class="action-xseries-icon" aria-hidden="true"></span>
${_("View {category} Details").format(category=display_category)}
${_("View {category} Details").format(category=category)}
</a>
</div>
</div>
<div class="certificate-container">
<h2 class="hd-6"><%- gettext('XSeries Program Certificates') %>:</h2>
<img src="<%- xseriesImage %>" alt="">
<h2 class="hd-6"><%- gettext('Program Certificates') %></h2>
<img src="<%- sampleCertImageSrc %>" alt="">
<% _.each(certificatesData, function(certificate){ %>
<a class="certificate-link" href="<%- gettext(certificate.credential_url) %>"><%- gettext(certificate.display_name) %></a>
<% }); %>
......
<section class="empty-programs-message">
<h2 class="hd-3"><%- gettext('You are not enrolled in any XSeries Programs yet.') %></h2>
<a class="btn-neutral find-xseries-programs" href="<%- xseriesUrl %>">
<span class="action-xseries-icon" aria-hidden="true"></span>
<span><%- gettext('Explore XSeries Programs') %></span>
<h2 class="hd-3"><%- gettext('You are not enrolled in any programs yet.') %></h2>
<a class="btn-neutral" href="<%- marketingUrl %>">
<span class="icon fa fa-search" aria-hidden="true"></span>
<span><%- gettext('Explore Programs') %></span>
</a>
</section>
......@@ -2,8 +2,8 @@
<%- gettext('Browse recently launched courses and see what\'s new in your favorite subjects') %>
</div>
<div class="ad-link">
<a href="<%- xseriesUrl %>" class="btn-neutral">
<a href="<%- marketingUrl %>" class="btn-neutral">
<span class="icon fa fa-search" aria-hidden="true"></span>
<span><%- gettext('Explore New XSeries') %></span>
<span><%- gettext('Explore New Programs') %></span>
</a>
</div>
......@@ -3,8 +3,8 @@
<div class="meta-info grid-container">
<div class="organization col"><%- orgList %></div>
<div class="category col col-last">
<span class="category-text"><%- gettext(type) %></span>
<span class="xseries-icon" aria-hidden="true"></span>
<span class="category-text"><%- gettext(category) %></span>
<span class="category-icon <%- category.toLowerCase() %>-icon" aria-hidden="true"></span>
</div>
</div>
<% if (progress) { %>
......
......@@ -14,11 +14,11 @@ from openedx.core.djangolib.js_utils import (
<%block name="js_extra">
<%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory">
ProgramListFactory({
programsData: ${programs | n, dump_js_escaped_json},
certificatesData: ${credentials | n, dump_js_escaped_json},
userProgress: ${progress | n, dump_js_escaped_json},
xseriesUrl: '${xseries_url | n, js_escaped_string}',
xseriesImage: '${static.url('images/xseries-certificate-visual.png') | n, js_escaped_string}'
marketingUrl: '${marketing_url | n, js_escaped_string}',
programsData: ${programs | n, dump_js_escaped_json},
sampleCertImageSrc: '${static.url('images/programs/sample-cert.png') | n, js_escaped_string}',
userProgress: ${progress | n, dump_js_escaped_json}
});
</%static:require_module>
</%block>
......
......@@ -176,44 +176,9 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user, category='xseries')
expected = self.expected_credentials_display_data()
# Checking result is as expected
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_programs_credentials_category(self):
""" Verify behaviour when program category is provided."""
# create credentials and program configuration
self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user, category='dummy_category')
expected = self.expected_credentials_display_data()
self.assertEqual(len(actual), 0)
actual = get_programs_credentials(self.user, category='xseries')
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_programs_credentials_no_category(self):
""" Verify behaviour when no program category is provided. """
self.create_credentials_config()
self.create_programs_config()
# Mocking the API responses from programs and credentials
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_programs_credentials(self.user)
expected = self.expected_credentials_display_data()
# Checking result is as expected
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
......@@ -66,7 +66,7 @@ def get_user_program_credentials(user):
return programs_credentials_data
def get_programs_credentials(user, category=None):
def get_programs_credentials(user):
"""Return program credentials data required for display.
Given a user, find all programs for which certificates have been earned
......@@ -74,7 +74,6 @@ def get_programs_credentials(user, category=None):
Arguments:
user (User): user object for getting programs credentials.
category(str) : program category for getting credentials.
Returns:
list of dict, containing data corresponding to the programs for which
......@@ -83,16 +82,14 @@ def get_programs_credentials(user, category=None):
programs_credentials = get_user_program_credentials(user)
credentials_data = []
for program in programs_credentials:
is_included = (category is None) or (program.get('category') == category)
if is_included:
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
return credentials_data
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('programs', '0008_programsapiconfig_program_details_enabled'),
]
operations = [
migrations.AddField(
model_name='programsapiconfig',
name='marketing_path',
field=models.CharField(help_text='Path used to construct URLs to programs marketing pages (e.g., "/foo").', max_length=255, blank=True),
),
]
......@@ -21,6 +21,14 @@ class ProgramsApiConfig(ConfigurationModel):
internal_service_url = models.URLField(verbose_name=_("Internal Service URL"))
public_service_url = models.URLField(verbose_name=_("Public Service URL"))
marketing_path = models.CharField(
max_length=255,
blank=True,
help_text=_(
'Path used to construct URLs to programs marketing pages (e.g., "/foo").'
)
)
# TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995
authoring_app_js_path = models.CharField(
verbose_name=_("Path to authoring app's JS"),
......@@ -73,6 +81,7 @@ class ProgramsApiConfig(ConfigurationModel):
)
)
# TODO: Remove unused field.
xseries_ad_enabled = models.BooleanField(
verbose_name=_("Do we want to show xseries program advertising"),
default=False
......@@ -132,13 +141,6 @@ class ProgramsApiConfig(ConfigurationModel):
return self.enabled and self.enable_certification
@property
def show_xseries_ad(self):
"""
Indicates whether we should show xseries add
"""
return self.enabled and self.xseries_ad_enabled
@property
def show_program_listing(self):
"""
Indicates whether we want to show program listing page
......
......@@ -13,7 +13,7 @@ class Program(factory.Factory):
id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name
name = FuzzyText(prefix='Program ')
subtitle = FuzzyText(prefix='Subtitle ')
category = 'xseries'
category = 'FooBar'
status = 'unpublished'
marketing_slug = FuzzyText(prefix='slug_')
organizations = []
......
......@@ -19,9 +19,9 @@ class ProgramsApiConfigMixin(object):
'enable_student_dashboard': True,
'enable_studio_tab': True,
'enable_certification': True,
'xseries_ad_enabled': True,
'program_listing_enabled': True,
'program_details_enabled': True,
'marketing_path': 'foo',
}
def create_programs_config(self, **kwargs):
......
......@@ -93,24 +93,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
# Verify the API was actually hit (not the cache).
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@ddt.data(True, False)
def test_get_programs_category_casing(self, is_detail):
"""Temporary. Verify that program categories are lowercased."""
self.create_programs_config()
program = factories.Program(category='camelCase')
if is_detail:
program_id = program['id']
self.mock_programs_api(data=program, program_id=program_id)
data = utils.get_programs(self.user, program_id=program_id)
self.assertEqual(data['category'], 'camelcase')
else:
self.mock_programs_api(data={'results': [program]})
data = utils.get_programs(self.user)
self.assertEqual(data[0]['category'], 'camelcase')
def test_get_programs_caching(self):
"""Verify that when enabled, the cache is used for non-staff users."""
self.create_programs_config(cache_ttl=1)
......@@ -235,18 +217,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential
actual = utils.get_programs_for_credentials(self.user, credential_data)
self.assertEqual(actual, [])
def test_get_display_category_success(self):
self.create_programs_config()
self.mock_programs_api()
actual_programs = utils.get_programs(self.user)
for program in actual_programs:
expected = 'XSeries'
self.assertEqual(expected, utils.get_display_category(program))
def test_get_display_category_none(self):
self.assertEqual('', utils.get_display_category(None))
self.assertEqual('', utils.get_display_category({"id": "test"}))
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class GetCompletedCoursesTestCase(TestCase):
......
......@@ -49,16 +49,7 @@ def get_programs(user, program_id=None):
# to see them displayed immediately.
cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
data = get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
# TODO: Temporary, to be removed once category names are cased for display. ECOM-5018.
if data and program_id:
data['category'] = data['category'].lower()
else:
for program in data:
program['category'] = program['category'].lower()
return data
return get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key)
def flatten_programs(programs, course_ids):
......@@ -151,7 +142,7 @@ def get_program_detail_url(program, marketing_root):
Arguments:
program (dict): Representation of a program.
marketing_root (str): Root URL used to build links to XSeries marketing pages.
marketing_root (str): Root URL used to build links to program marketing pages.
Returns:
str, a link to program details
......@@ -166,24 +157,6 @@ def get_program_detail_url(program, marketing_root):
return '{base}/{slug}'.format(base=base, slug=slug)
def get_display_category(program):
""" Given the program, return the category of the program for display
Arguments:
program (Program): The program to get the display category string from
Returns:
string, the category for display to the user.
Empty string if the program has no category or is null.
"""
display_candidate = ''
if program and program.get('category'):
if program.get('category') == 'xseries':
display_candidate = 'XSeries'
else:
display_candidate = program.get('category', '').capitalize()
return display_candidate
def get_completed_courses(student):
"""
Determine which courses have been completed by the user.
......
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