Merge pull request #13125 from edx/renzo/multiple-program-types
Allow program listing page to display programs from any category
This commit is contained in:
@@ -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)
|
||||
|
||||
7
lms/static/images/programs/micromasters-icon.svg
Normal file
7
lms/static/images/programs/micromasters-icon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
10
lms/static/images/programs/xseries-icon.svg
Normal file
10
lms/static/images/programs/xseries-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
@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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<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>
|
||||
<% }); %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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')
|
||||
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)
|
||||
|
||||
@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()
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- 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
|
||||
@@ -131,13 +140,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):
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user