diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index a16101e928..4094a456ec 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -891,7 +891,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 the ProgramsDataMixin. +# TODO: Clean up these tests so that they use program factories. @attr('shard_3') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 5843c0b91c..835cf2f45e 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -1,268 +1,324 @@ +# -*- coding: utf-8 -*- """ Unit tests covering the program listing and detail pages. """ -import datetime import json +import re import unittest from urlparse import urljoin from bs4 import BeautifulSoup from django.conf import settings from django.core.urlresolvers import reverse -from django.test import override_settings, TestCase +from django.test import override_settings +from django.utils.text import slugify from edx_oauth2_provider.tests.factories import ClientFactory import httpretty -from opaque_keys.edx import locator from provider.constants import CONFIDENTIAL from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.tests import factories as credentials_factories -from openedx.core.djangoapps.credentials.tests.mixins import CredentialsDataMixin, CredentialsApiConfigMixin +from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.djangoapps.programs.tests import factories -from openedx.core.djangoapps.programs.tests.mixins import ( - ProgramsApiConfigMixin, - ProgramsDataMixin) -from student.models import CourseEnrollment -from student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase +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 +@httpretty.activate +@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'}) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) -class TestProgramListing( - ModuleStoreTestCase, - ProgramsApiConfigMixin, - ProgramsDataMixin, - CredentialsDataMixin, - CredentialsApiConfigMixin): - - """ - Unit tests for getting the list of programs enrolled by a logged in user - """ - PASSWORD = 'test' +class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, SharedModuleStoreTestCase): + """Unit tests for the program listing page.""" + maxDiff = None + password = 'test' url = reverse('program_listing_view') - def setUp(self): - """ - Add a student - """ - super(TestProgramListing, self).setUp() - ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - self.student = UserFactory() + @classmethod + def setUpClass(cls): + super(TestProgramListing, cls).setUpClass() - def _create_course_and_enroll(self, student, org, course, run): - """ - Creates a course and associated enrollment. - """ - course_location = locator.CourseLocator(org, course, run) - course = CourseFactory.create( - org=course_location.org, - number=course_location.course, - run=course_location.run + for name in [ProgramsApiConfig.OAUTH2_CLIENT_NAME, CredentialsApiConfig.OAUTH2_CLIENT_NAME]: + ClientFactory(name=name, client_type=CONFIDENTIAL) + + cls.course = CourseFactory() + organization = programs_factories.Organization() + run_mode = programs_factories.RunMode(course_key=unicode(cls.course.id)) # pylint: disable=no-member + course_code = programs_factories.CourseCode(run_modes=[run_mode]) + + cls.first_program = programs_factories.Program( + organizations=[organization], + course_codes=[course_code] + ) + cls.second_program = programs_factories.Program( + organizations=[organization], + course_codes=[course_code] ) - enrollment = CourseEnrollment.enroll(student, course.id) - enrollment.created = datetime.datetime(2000, 12, 31, 0, 0, 0, 0) - enrollment.save() - def _get_program_url(self, marketing_slug): - """ - Helper function to get the program card url - """ - return urljoin( - settings.MKTG_URLS.get('ROOT'), - 'xseries' + '/{}' - ).format(marketing_slug) + cls.data = sorted([cls.first_program, cls.second_program], key=cls.program_sort_key) - def _setup_and_get_program(self): - """ - The core function to setup the mock program api, - then call the django test client to get the actual program listing page - make sure the request suceeds and make sure x_series_url is on the page - """ - self.mock_programs_api() - self.client.login(username=self.student.username, password=self.PASSWORD) - response = self.client.get(self.url) - x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries') - self.assertContains(response, x_series_url) - return response + cls.marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').rstrip('/') - def _get_program_checklist(self, program_id): - """ - The convenience function to get all the program related page element we would like to check against - """ - return [ - self.PROGRAM_NAMES[program_id], - self._get_program_url(self.PROGRAMS_API_RESPONSE['results'][program_id]['marketing_slug']), - self.PROGRAMS_API_RESPONSE['results'][program_id]['organizations'][0]['display_name'], - ] + def setUp(self): + super(TestProgramListing, self).setUp() - def _assert_progress_data_present(self, response): - """Verify that progress data is present.""" - self.assertContains(response, 'userProgress') + self.user = UserFactory() + self.client.login(username=self.user.username, password=self.password) - @httpretty.activate - def test_get_program_with_no_enrollment(self): + @classmethod + def program_sort_key(cls, program): + """ + Helper function used to sort dictionaries representing programs. + """ + return program['id'] + + def credential_sort_key(self, credential): + """ + Helper function used to sort dictionaries representing credentials. + """ + try: + return credential['certificate_url'] + except KeyError: + return credential['credential_url'] + + def mock_programs_api(self, data): + """Helper for mocking out Programs API URLs.""" + self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.') + + url = ProgramsApiConfig.current().internal_api_url.strip('/') + '/programs/' + body = json.dumps({'results': data}) + + httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json') + + def mock_credentials_api(self, data): + """Helper for mocking out Credentials API URLs.""" + self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.') + + url = '{base}/user_credentials/?username={username}'.format( + base=CredentialsApiConfig.current().internal_api_url.strip('/'), + username=self.user.username + ) + body = json.dumps({'results': data}) + + httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json') + + def load_serialized_data(self, response, key): + """ + Extract and deserialize serialized data from the response. + """ + pattern = re.compile(r'{key}: (?P\[.*\])'.format(key=key)) + match = pattern.search(response.content) + serialized = match.group('data') + + return json.loads(serialized) + + def assert_dict_contains_subset(self, superset, subset): + """ + Verify that the dict superset contains the dict subset. + + Works like assertDictContainsSubset, deprecated since Python 3.2. + See: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertDictContainsSubset. + """ + superset_keys = set(superset.keys()) + subset_keys = set(subset.keys()) + intersection = {key: superset[key] for key in superset_keys & subset_keys} + + self.assertEqual(subset, intersection) + + def test_login_required(self): + """ + Verify that login is required to access the page. + """ self.create_programs_config() - response = self._setup_and_get_program() - for program_element in self._get_program_checklist(0): - self.assertNotContains(response, program_element) - for program_element in self._get_program_checklist(1): - self.assertNotContains(response, program_element) + self.mock_programs_api(self.data) - @httpretty.activate - def test_get_one_program(self): - self.create_programs_config() - self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/')) - response = self._setup_and_get_program() - for program_element in self._get_program_checklist(0): - self.assertContains(response, program_element) - for program_element in self._get_program_checklist(1): - self.assertNotContains(response, program_element) + self.client.logout() - self._assert_progress_data_present(response) - - @httpretty.activate - def test_get_both_program(self): - self.create_programs_config() - self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/')) - self._create_course_and_enroll(self.student, *self.COURSE_KEYS[5].split('/')) - response = self._setup_and_get_program() - for program_element in self._get_program_checklist(0): - self.assertContains(response, program_element) - for program_element in self._get_program_checklist(1): - self.assertContains(response, program_element) - - self._assert_progress_data_present(response) - - def test_get_programs_dashboard_not_enabled(self): - self.create_programs_config(program_listing_enabled=False) - self.client.login(username=self.student.username, password=self.PASSWORD) response = self.client.get(self.url) - self.assertEqual(response.status_code, 404) - - def test_xseries_advertise_disabled(self): - self.create_programs_config(xseries_ad_enabled=False) - self.client.login(username=self.student.username, password=self.PASSWORD) - response = self.client.get(self.url) - x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries') - self.assertNotContains(response, x_series_url) - - def test_get_programs_not_logged_in(self): - self.create_programs_config() - response = self.client.get(self.url) - self.assertRedirects( response, '{}?next={}'.format(reverse('signin_user'), self.url) ) - def _expected_progam_credentials_data(self): + self.client.login(username=self.user.username, password=self.password) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_404_if_disabled(self): """ - Dry method for getting expected program credentials response data. + Verify that the page 404s if disabled. """ - return [ - credentials_factories.UserCredential( - id=1, - username='test', - credential=credentials_factories.ProgramCredential() - ), - credentials_factories.UserCredential( - id=2, - username='test', - credential=credentials_factories.ProgramCredential() + self.create_programs_config(program_listing_enabled=False) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) + + def test_empty_state(self): + """ + Verify that the response contains no programs data when no programs are engaged. + """ + self.create_programs_config() + self.mock_programs_api(self.data) + + response = self.client.get(self.url) + self.assertContains(response, 'programsData: []') + + def test_programs_listed(self): + """ + Verify that the response contains accurate programs data when programs are engaged. + """ + self.create_programs_config() + self.mock_programs_api(self.data) + + CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member + + response = self.client.get(self.url) + actual = self.load_serialized_data(response, 'programsData') + actual = sorted(actual, key=self.program_sort_key) + + 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 _expected_credentials_data(self): - """ Dry method for getting expected credentials.""" - program_credentials_data = self._expected_progam_credentials_data() - return [ - { - 'display_name': self.PROGRAMS_API_RESPONSE['results'][0]['name'], - 'subtitle': self.PROGRAMS_API_RESPONSE['results'][0]['subtitle'], - 'credential_url':program_credentials_data[0]['certificate_url'] - }, - { - 'display_name': self.PROGRAMS_API_RESPONSE['results'][1]['name'], - 'subtitle':self.PROGRAMS_API_RESPONSE['results'][1]['subtitle'], - 'credential_url':program_credentials_data[1]['certificate_url'] - } - ] + def test_toggle_xseries_advertising(self): + """ + Verify that when XSeries advertising is disabled, no link to the marketing site + appears in the response (and vice versa). + """ + # Verify the URL is present when advertising is enabled. + self.create_programs_config() + self.mock_programs_api(self.data) - @httpretty.activate - def test_get_xseries_certificates_with_data(self): + 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) + + response = self.client.get(self.url) + self.assertNotContains(response, self.marketing_root) + + def test_links_to_detail_pages(self): + """ + Verify that links to detail pages are present when enabled, instead of + links to the marketing site. + """ + self.create_programs_config() + self.mock_programs_api(self.data) + + CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member + + response = self.client.get(self.url) + actual = self.load_serialized_data(response, 'programsData') + actual = sorted(actual, key=self.program_sort_key) + + for index, actual_program in enumerate(actual): + expected_program = self.data[index] + + base = reverse('program_details_view', args=[expected_program['id']]).rstrip('/') + slug = slugify(expected_program['name']) + self.assertEqual( + actual_program['detail_url'], + '{}/{}'.format(base, slug) + ) + + # Verify that links to the marketing site are present when detail pages are disabled. + self.create_programs_config(program_details_enabled=False) + + response = self.client.get(self.url) + actual = self.load_serialized_data(response, 'programsData') + actual = sorted(actual, key=self.program_sort_key) + + for index, actual_program in enumerate(actual): + expected_program = self.data[index] + + self.assertEqual( + actual_program['detail_url'], + '{}/{}'.format(self.marketing_root, expected_program['marketing_slug']) + ) + + def test_certificates_listed(self): + """ + Verify that the response contains accurate certificate data when certificates are available. + """ self.create_programs_config() self.create_credentials_config(is_learner_issuance_enabled=True) - self.client.login(username=self.student.username, password=self.PASSWORD) + self.mock_programs_api(self.data) - # mock programs and credentials apis - self.mock_programs_api() - self.mock_credentials_api(self.student, data=self.CREDENTIALS_API_RESPONSE, reset_url=False) + first_credential = credentials_factories.UserCredential( + username=self.user.username, + credential=credentials_factories.ProgramCredential( + program_id=self.first_program['id'] + ) + ) + second_credential = credentials_factories.UserCredential( + username=self.user.username, + credential=credentials_factories.ProgramCredential( + program_id=self.second_program['id'] + ) + ) - response = self.client.get(reverse("program_listing_view")) - for certificate in self._expected_credentials_data(): - self.assertContains(response, certificate['display_name']) - self.assertContains(response, certificate['credential_url']) + credentials_data = sorted([first_credential, second_credential], key=self.credential_sort_key) - self.assertContains(response, 'images/xseries-certificate-visual.png') + self.mock_credentials_api(credentials_data) - @httpretty.activate - def test_get_xseries_certificates_without_data(self): + response = self.client.get(self.url) + actual = self.load_serialized_data(response, 'certificatesData') + actual = sorted(actual, key=self.credential_sort_key) - self.create_programs_config() - self.create_credentials_config(is_learner_issuance_enabled=True) + for index, actual_credential in enumerate(actual): + expected_credential = credentials_data[index] - self.client.login(username=self.student.username, password=self.PASSWORD) - - # mock programs and credentials apis - self.mock_programs_api() - self.mock_credentials_api(self.student, data={"results": []}, reset_url=False) - - response = self.client.get(reverse("program_listing_view")) - for certificate in self._expected_credentials_data(): - self.assertNotContains(response, certificate['display_name']) - self.assertNotContains(response, certificate['credential_url']) + self.assertEqual( + # TODO: certificate_url is needlessly transformed to credential_url. (╯°□°)╯︵ ┻━┻ + # Clean this up! + actual_credential['credential_url'], + expected_credential['certificate_url'] + ) @httpretty.activate -@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) +@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'}) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): - """ - Unit tests for the program details page - """ + """Unit tests for the program details page.""" program_id = 123 password = 'test' + url = reverse('program_details_view', args=[program_id]) @classmethod def setUpClass(cls): super(TestProgramDetails, cls).setUpClass() - cls.course = CourseFactory() + + ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) + + course = CourseFactory() + organization = programs_factories.Organization() + run_mode = programs_factories.RunMode(course_key=unicode(course.id)) # pylint: disable=no-member + course_code = programs_factories.CourseCode(run_modes=[run_mode]) + + cls.data = programs_factories.Program( + organizations=[organization], + course_codes=[course_code] + ) def setUp(self): super(TestProgramDetails, self).setUp() - self.details_page = reverse('program_details_view', args=[self.program_id]) - self.user = UserFactory() self.client.login(username=self.user.username, password=self.password) - ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - - self.organization = factories.Organization() - self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member - self.course_code = factories.CourseCode(run_modes=[self.run_mode]) - self.data = factories.Program( - organizations=[self.organization], - course_codes=[self.course_code] - ) - - def _mock_programs_api(self, data, status=200): + def mock_programs_api(self, data, status=200): """Helper for mocking out Programs API URLs.""" self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.') @@ -281,15 +337,15 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): content_type='application/json', ) - def _assert_program_data_present(self, response): + def assert_program_data_present(self, response): """Verify that program data is present.""" self.assertContains(response, 'programData') self.assertContains(response, 'urls') self.assertContains(response, 'program_listing_url') self.assertContains(response, self.data['name']) - self._assert_programs_tab_present(response) + self.assert_programs_tab_present(response) - def _assert_programs_tab_present(self, response): + def assert_programs_tab_present(self, response): """Verify that the programs tab is present in the nav.""" soup = BeautifulSoup(response.content, 'html.parser') self.assertTrue( @@ -301,20 +357,20 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): Verify that login is required to access the page. """ self.create_programs_config() - self._mock_programs_api(self.data) + self.mock_programs_api(self.data) self.client.logout() - response = self.client.get(self.details_page) + response = self.client.get(self.url) self.assertRedirects( response, - '{}?next={}'.format(reverse('signin_user'), self.details_page) + '{}?next={}'.format(reverse('signin_user'), self.url) ) self.client.login(username=self.user.username, password=self.password) - response = self.client.get(self.details_page) - self._assert_program_data_present(response) + response = self.client.get(self.url) + self.assert_program_data_present(response) def test_404_if_disabled(self): """ @@ -322,33 +378,33 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): """ self.create_programs_config(program_details_enabled=False) - response = self.client.get(self.details_page) - self.assertEquals(response.status_code, 404) - - def test_page_routing(self): - """Verify that the page can be hit with or without a program name in the URL.""" - self.create_programs_config() - self._mock_programs_api(self.data) - - response = self.client.get(self.details_page) - self._assert_program_data_present(response) - - response = self.client.get(self.details_page + 'program_name/') - self._assert_program_data_present(response) - - response = self.client.get(self.details_page + 'program_name/invalid/') - self.assertEquals(response.status_code, 404) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) def test_404_if_no_data(self): """Verify that the page 404s if no program data is found.""" self.create_programs_config() - self._mock_programs_api(self.data, status=404) - response = self.client.get(self.details_page) - self.assertEquals(response.status_code, 404) + self.mock_programs_api(self.data, status=404) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) httpretty.reset() - self._mock_programs_api({}) - response = self.client.get(self.details_page) - self.assertEquals(response.status_code, 404) + self.mock_programs_api({}) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) + + def test_page_routing(self): + """Verify that the page can be hit with or without a program name in the URL.""" + self.create_programs_config() + self.mock_programs_api(self.data) + + response = self.client.get(self.url) + self.assert_program_data_present(response) + + response = self.client.get(self.url + 'program_name/') + self.assert_program_data_present(response) + + response = self.client.get(self.url + 'program_name/invalid/') + self.assertEqual(response.status_code, 404) diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 9b210d8e5c..4ba5cab92c 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -21,28 +21,26 @@ from lms.djangoapps.learner_dashboard.utils import ( @require_GET def view_programs(request): """View programs in which the user is engaged.""" - show_program_listing = ProgramsApiConfig.current().show_program_listing - if not show_program_listing: + programs_config = ProgramsApiConfig.current() + if not programs_config.show_program_listing: raise Http404 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').strip('/') + marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').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['marketing_url'] = '{root}/{slug}'.format( - root=marketing_root, - slug=program['marketing_slug'] - ) context = { 'programs': programs, 'progress': meter.progress, - 'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None, + 'xseries_url': marketing_root if programs_config.show_xseries_ad else None, 'nav_hidden': True, - 'show_program_listing': show_program_listing, + 'show_program_listing': programs_config.show_program_listing, 'credentials': get_programs_credentials(request.user, category='xseries'), 'disable_courseware_js': True, 'uses_pattern_library': True @@ -55,8 +53,8 @@ def view_programs(request): @require_GET def program_details(request, program_id): """View details about a specific program.""" - show_program_details = ProgramsApiConfig.current().show_program_details - if not show_program_details: + programs_config = ProgramsApiConfig.current() + if not programs_config.show_program_details: raise Http404 program_data = utils.get_programs(request.user, program_id=program_id) @@ -65,7 +63,6 @@ def program_details(request, program_id): raise Http404 program_data = utils.supplement_program_data(program_data, request.user) - show_program_listing = ProgramsApiConfig.current().show_program_listing urls = { 'program_listing_url': reverse('program_listing_view'), @@ -77,7 +74,7 @@ def program_details(request, program_id): context = { 'program_data': program_data, 'urls': urls, - 'show_program_listing': show_program_listing, + 'show_program_listing': programs_config.show_program_listing, 'nav_hidden': True, 'disable_courseware_js': True, 'uses_pattern_library': True diff --git a/lms/static/js/learner_dashboard/models/program_model.js b/lms/static/js/learner_dashboard/models/program_model.js index 0cfefbf9ab..cd5e5811ee 100644 --- a/lms/static/js/learner_dashboard/models/program_model.js +++ b/lms/static/js/learner_dashboard/models/program_model.js @@ -15,7 +15,7 @@ type: data.display_category + ' Program', subtitle: data.subtitle, organizations: data.organizations, - marketingUrl: data.marketing_url, + detailUrl: data.detail_url, smallBannerUrl: data.banner_image_urls.w348h116, mediumBannerUrl: data.banner_image_urls.w435h145, largeBannerUrl: data.banner_image_urls.w726h242, 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 34b95c2cc3..e179a8e1fb 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 @@ -28,7 +28,7 @@ define([ modified: '2016-03-25T13:45:21.220732Z', marketing_slug: 'p_2?param=haha&test=b', id: 146, - marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b', + detail_url: 'http://courses.edx.org/dashboard/programs/1/foo', banner_image_urls: { w348h116: 'http://www.edx.org/images/test1', w435h145: 'http://www.edx.org/images/test2', @@ -55,7 +55,7 @@ define([ expect($card.find('.title').html().trim()).toEqual(program.name); expect($card.find('.category span').html().trim()).toEqual('XSeries Program'); expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key); - expect($card.find('.card-link').attr('href')).toEqual(program.marketing_url); + expect($card.find('.card-link').attr('href')).toEqual(program.detail_url); }; beforeEach(function() { diff --git a/lms/templates/learner_dashboard/empty_programs_list.underscore b/lms/templates/learner_dashboard/empty_programs_list.underscore index 2215da497d..77a233ab97 100644 --- a/lms/templates/learner_dashboard/empty_programs_list.underscore +++ b/lms/templates/learner_dashboard/empty_programs_list.underscore @@ -5,4 +5,3 @@ <%- gettext('Explore XSeries Programs') %> - diff --git a/lms/templates/learner_dashboard/program_card.underscore b/lms/templates/learner_dashboard/program_card.underscore index 6387f52216..1c21e7ce51 100644 --- a/lms/templates/learner_dashboard/program_card.underscore +++ b/lms/templates/learner_dashboard/program_card.underscore @@ -1,4 +1,3 @@ -

<%- gettext(name) %>

@@ -10,7 +9,7 @@
<% if (progress) { %>

- <%= interpolate( + <%= interpolate( ngettext( '%(count)s course is in progress.', '%(count)s courses are in progress.', @@ -19,7 +18,7 @@ {count: progress.total.in_progress}, true ) %> - <%= interpolate( + <%= interpolate( ngettext( '%(count)s course has not been started.', '%(count)s courses have not been started.', @@ -42,7 +41,7 @@

<% } %> - +