Commit c5434c53 by Renzo Lucioni Committed by GitHub

Merge pull request #13125 from edx/renzo/multiple-program-types

Allow program listing page to display programs from any category
parents 13c8e392 8196e1a0
......@@ -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