From 8196e1a02416f36a01f5a691899351fb3d8dda1f Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Mon, 25 Jul 2016 15:38:50 -0400 Subject: [PATCH] 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. --- common/djangoapps/student/tests/tests.py | 20 ++++++---- common/djangoapps/student/views.py | 5 +-- common/test/acceptance/fixtures/programs.py | 1 - .../learner_dashboard/tests/test_programs.py | 29 ++++---------- lms/djangoapps/learner_dashboard/urls.py | 2 +- lms/djangoapps/learner_dashboard/views.py | 25 +++++------- .../images/programs/micromasters-icon.svg | 7 ++++ .../sample-cert.png} | Bin lms/static/images/programs/xseries-icon.svg | 10 +++++ .../learner_dashboard/models/program_model.js | 2 +- .../learner_dashboard/program_list_factory.js | 10 +++-- .../views/collection_list_view.js | 4 +- .../views/explore_new_programs_view.js | 4 +- .../certificate_view_spec.js | 6 +-- .../program_card_view_spec.js | 5 +-- .../learner_dashboard/sidebar_view_spec.js | 12 +++--- lms/static/sass/_build-lms-v1.scss | 1 - lms/static/sass/elements/_program-card.scss | 36 ++++++++++------- .../sass/elements/_xseries-certificates.scss | 17 -------- lms/static/sass/views/_program-list.scss | 28 ------------- .../dashboard/_dashboard_course_listing.html | 8 ++-- ...info.html => _dashboard_program_info.html} | 8 ++-- .../learner_dashboard/certificate.underscore | 4 +- .../empty_programs_list.underscore | 8 ++-- .../explore_new_programs.underscore | 4 +- .../learner_dashboard/program_card.underscore | 4 +- lms/templates/learner_dashboard/programs.html | 8 ++-- .../credentials/tests/test_utils.py | 37 +----------------- openedx/core/djangoapps/credentials/utils.py | 23 +++++------ .../0009_programsapiconfig_marketing_path.py | 19 +++++++++ openedx/core/djangoapps/programs/models.py | 16 ++++---- .../djangoapps/programs/tests/factories.py | 2 +- .../core/djangoapps/programs/tests/mixins.py | 2 +- .../djangoapps/programs/tests/test_utils.py | 30 -------------- openedx/core/djangoapps/programs/utils.py | 31 +-------------- 35 files changed, 159 insertions(+), 269 deletions(-) create mode 100644 lms/static/images/programs/micromasters-icon.svg rename lms/static/images/{xseries-certificate-visual.png => programs/sample-cert.png} (100%) create mode 100644 lms/static/images/programs/xseries-icon.svg delete mode 100644 lms/static/sass/elements/_xseries-certificates.scss rename lms/templates/dashboard/{_dashboard_xseries_info.html => _dashboard_program_info.html} (87%) create mode 100644 openedx/core/djangoapps/programs/migrations/0009_programsapiconfig_marketing_path.py diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index ba19c37062..9356a4a399 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -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): diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 2b7779073a..f9a0595e41 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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) diff --git a/common/test/acceptance/fixtures/programs.py b/common/test/acceptance/fixtures/programs.py index baaea09180..c33d4c2459 100644 --- a/common/test/acceptance/fixtures/programs.py +++ b/common/test/acceptance/fixtures/programs.py @@ -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() diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 5c96412937..845da498a4 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -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): diff --git a/lms/djangoapps/learner_dashboard/urls.py b/lms/djangoapps/learner_dashboard/urls.py index 91f9555c3e..fc8aee085f 100644 --- a/lms/djangoapps/learner_dashboard/urls.py +++ b/lms/djangoapps/learner_dashboard/urls.py @@ -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\d+)/[\w\-]*/?$', views.program_details, name='program_details_view'), ] diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 5e25b8718d..7f33442dde 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -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) diff --git a/lms/static/images/programs/micromasters-icon.svg b/lms/static/images/programs/micromasters-icon.svg new file mode 100644 index 0000000000..e17bb4ac72 --- /dev/null +++ b/lms/static/images/programs/micromasters-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lms/static/images/xseries-certificate-visual.png b/lms/static/images/programs/sample-cert.png similarity index 100% rename from lms/static/images/xseries-certificate-visual.png rename to lms/static/images/programs/sample-cert.png diff --git a/lms/static/images/programs/xseries-icon.svg b/lms/static/images/programs/xseries-icon.svg new file mode 100644 index 0000000000..af09119d25 --- /dev/null +++ b/lms/static/images/programs/xseries-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lms/static/js/learner_dashboard/models/program_model.js b/lms/static/js/learner_dashboard/models/program_model.js index cd5e5811ee..c2743255f0 100644 --- a/lms/static/js/learner_dashboard/models/program_model.js +++ b/lms/static/js/learner_dashboard/models/program_model.js @@ -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, diff --git a/lms/static/js/learner_dashboard/program_list_factory.js b/lms/static/js/learner_dashboard/program_list_factory.js index 06fd63cbdf..217d6045fd 100644 --- a/lms/static/js/learner_dashboard/program_list_factory.js +++ b/lms/static/js/learner_dashboard/program_list_factory.js @@ -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); diff --git a/lms/static/js/learner_dashboard/views/collection_list_view.js b/lms/static/js/learner_dashboard/views/collection_list_view.js index f0e2af1cf0..1f902bb58b 100644 --- a/lms/static/js/learner_dashboard/views/collection_list_view.js +++ b/lms/static/js/learner_dashboard/views/collection_list_view.js @@ -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 { diff --git a/lms/static/js/learner_dashboard/views/explore_new_programs_view.js b/lms/static/js/learner_dashboard/views/explore_new_programs_view.js index 1a403c4664..4ebe746ed1 100644 --- a/lms/static/js/learner_dashboard/views/explore_new_programs_view.js +++ b/lms/static/js/learner_dashboard/views/explore_new_programs_view.js @@ -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 { /** diff --git a/lms/static/js/spec/learner_dashboard/certificate_view_spec.js b/lms/static/js/spec/learner_dashboard/certificate_view_spec.js index 7378c9cf4a..868ddde4c9 100644 --- a/lms/static/js/spec/learner_dashboard/certificate_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/certificate_view_spec.js @@ -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() { diff --git a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js index e179a8e1fb..2bd1ed91f7 100644 --- a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js @@ -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); }; diff --git a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js index e2995f0f11..c76b1f7e87 100644 --- a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js @@ -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({ diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index 437eda54a4..5a4d609076 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -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 diff --git a/lms/static/sass/elements/_program-card.scss b/lms/static/sass/elements/_program-card.scss index 044416ea86..177dc9b1d4 100644 --- a/lms/static/sass/elements/_program-card.scss +++ b/lms/static/sass/elements/_program-card.scss @@ -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); diff --git a/lms/static/sass/elements/_xseries-certificates.scss b/lms/static/sass/elements/_xseries-certificates.scss deleted file mode 100644 index 33fcd8f6ad..0000000000 --- a/lms/static/sass/elements/_xseries-certificates.scss +++ /dev/null @@ -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; - } -} diff --git a/lms/static/sass/views/_program-list.scss b/lms/static/sass/views/_program-list.scss index 2bf6d30bc6..2b02d20545 100644 --- a/lms/static/sass/views/_program-list.scss +++ b/lms/static/sass/views/_program-list.scss @@ -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; - } - } - } } diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index a2a24c4006..54399f2d21 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -53,10 +53,10 @@ from student.helpers import ( <% mode_class = '' %> % endif
- % if course_program_info and course_program_info.get('category')=='xseries': + % if course_program_info and course_program_info.get('category')=='XSeries':
-

${_("{category} Program Course").format(category=course_program_info['display_category'])}

+

${_("{category} Program Course").format(category=course_program_info['category'])}

% endif
@@ -370,9 +370,9 @@ from student.helpers import (
%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 diff --git a/lms/templates/dashboard/_dashboard_xseries_info.html b/lms/templates/dashboard/_dashboard_program_info.html similarity index 87% rename from lms/templates/dashboard/_dashboard_xseries_info.html rename to lms/templates/dashboard/_dashboard_program_info.html index 923832cdc1..5615fb3eb3 100644 --- a/lms/templates/dashboard/_dashboard_xseries_info.html +++ b/lms/templates/dashboard/_dashboard_program_info.html @@ -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 @@

- ${_("{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)}

${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('').format(program_data['program_marketing_url']), link_end=HTML(''), program_display_name=program_data['display_name'], - program_category=display_category, + program_category=category, )}

@@ -30,7 +30,7 @@ data-program-id="${program_data['program_id']}" > ${program_data['display_name']} - ${_("View {category} Details").format(category=display_category)} + ${_("View {category} Details").format(category=category)}
diff --git a/lms/templates/learner_dashboard/certificate.underscore b/lms/templates/learner_dashboard/certificate.underscore index 1110dc76de..f7b85ddf90 100644 --- a/lms/templates/learner_dashboard/certificate.underscore +++ b/lms/templates/learner_dashboard/certificate.underscore @@ -1,6 +1,6 @@
-

<%- gettext('XSeries Program Certificates') %>:

- +

<%- gettext('Program Certificates') %>

+ <% _.each(certificatesData, function(certificate){ %> <%- gettext(certificate.display_name) %> <% }); %> diff --git a/lms/templates/learner_dashboard/empty_programs_list.underscore b/lms/templates/learner_dashboard/empty_programs_list.underscore index 77a233ab97..d29afed873 100644 --- a/lms/templates/learner_dashboard/empty_programs_list.underscore +++ b/lms/templates/learner_dashboard/empty_programs_list.underscore @@ -1,7 +1,7 @@
-

<%- gettext('You are not enrolled in any XSeries Programs yet.') %>

- - - <%- gettext('Explore XSeries Programs') %> +

<%- gettext('You are not enrolled in any programs yet.') %>

+
+ + <%- gettext('Explore Programs') %>
diff --git a/lms/templates/learner_dashboard/explore_new_programs.underscore b/lms/templates/learner_dashboard/explore_new_programs.underscore index 00bc5afec3..af70c50dfe 100644 --- a/lms/templates/learner_dashboard/explore_new_programs.underscore +++ b/lms/templates/learner_dashboard/explore_new_programs.underscore @@ -2,8 +2,8 @@ <%- gettext('Browse recently launched courses and see what\'s new in your favorite subjects') %>
diff --git a/lms/templates/learner_dashboard/program_card.underscore b/lms/templates/learner_dashboard/program_card.underscore index 1c21e7ce51..c44ffbb3b1 100644 --- a/lms/templates/learner_dashboard/program_card.underscore +++ b/lms/templates/learner_dashboard/program_card.underscore @@ -3,8 +3,8 @@
<%- orgList %>
- <%- gettext(type) %> - + <%- gettext(category) %> +
<% if (progress) { %> diff --git a/lms/templates/learner_dashboard/programs.html b/lms/templates/learner_dashboard/programs.html index df89f3e9a9..29b4a75789 100644 --- a/lms/templates/learner_dashboard/programs.html +++ b/lms/templates/learner_dashboard/programs.html @@ -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} }); diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index 399ef697dc..e563b7f031 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -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) diff --git a/openedx/core/djangoapps/credentials/utils.py b/openedx/core/djangoapps/credentials/utils.py index 7f9cdaf361..fc5df22d8d 100644 --- a/openedx/core/djangoapps/credentials/utils.py +++ b/openedx/core/djangoapps/credentials/utils.py @@ -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 diff --git a/openedx/core/djangoapps/programs/migrations/0009_programsapiconfig_marketing_path.py b/openedx/core/djangoapps/programs/migrations/0009_programsapiconfig_marketing_path.py new file mode 100644 index 0000000000..b2bba1ba19 --- /dev/null +++ b/openedx/core/djangoapps/programs/migrations/0009_programsapiconfig_marketing_path.py @@ -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), + ), + ] diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py index 3f54d5694f..d903ab6578 100644 --- a/openedx/core/djangoapps/programs/models.py +++ b/openedx/core/djangoapps/programs/models.py @@ -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): """ diff --git a/openedx/core/djangoapps/programs/tests/factories.py b/openedx/core/djangoapps/programs/tests/factories.py index a20837a46f..f4e52e18e2 100644 --- a/openedx/core/djangoapps/programs/tests/factories.py +++ b/openedx/core/djangoapps/programs/tests/factories.py @@ -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 = [] diff --git a/openedx/core/djangoapps/programs/tests/mixins.py b/openedx/core/djangoapps/programs/tests/mixins.py index b3df487342..e13f92080e 100644 --- a/openedx/core/djangoapps/programs/tests/mixins.py +++ b/openedx/core/djangoapps/programs/tests/mixins.py @@ -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): diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index d20a4a3626..9649368236 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -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): diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index ebd6e5f802..8ec2173c7c 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -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.