diff --git a/cms/djangoapps/contentstore/views/program.py b/cms/djangoapps/contentstore/views/program.py index d95f10b9c5..0ac51d5df0 100644 --- a/cms/djangoapps/contentstore/views/program.py +++ b/cms/djangoapps/contentstore/views/program.py @@ -16,9 +16,8 @@ from openedx.core.lib.token_utils import JwtBuilder class ProgramAuthoringView(View): """View rendering a template which hosts the Programs authoring app. - The Programs authoring app is a Backbone SPA maintained in a separate repository. - The app handles its own routing and provides a UI which can be used to create and - publish new Programs (e.g, XSeries). + The Programs authoring app is a Backbone SPA. The app handles its own routing + and provides a UI which can be used to create and publish new Programs. """ @method_decorator(login_required) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 4e0f996ceb..b1d208433f 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -2,53 +2,56 @@ """ Miscellaneous tests for the student app. """ +from datetime import datetime, timedelta +import json import logging import unittest -import ddt -from datetime import datetime, timedelta from urlparse import urljoin -import pytz -from markupsafe import escape -from mock import Mock, patch -from nose.plugins.attrib import attr -from opaque_keys.edx.locations import SlashSeparatedCourseKey -from pyquery import PyQuery as pq - +import ddt from django.conf import settings from django.contrib.auth.models import User, AnonymousUser from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import Client +from edx_oauth2_provider.tests.factories import ClientFactory +import httpretty +from markupsafe import escape +from mock import Mock, patch +from nose.plugins.attrib import attr +from opaque_keys.edx.locations import SlashSeparatedCourseKey +from provider.constants import CONFIDENTIAL +from pyquery import PyQuery as pq +import pytz +from bulk_email.models import Optout # pylint: disable=import-error +from certificates.models import CertificateStatuses # pylint: disable=import-error +from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error +from config_models.models import cache from course_modes.models import CourseMode +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification +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 +import shoppingcart # pylint: disable=import-error from student.models import ( anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user, LinkedInAddToProfileConfiguration, UserAttribute ) +from student.tests.factories import UserFactory, CourseModeFactory, CourseEnrollmentFactory from student.views import ( process_survey_link, _cert_info, complete_course_mode_info, - _get_course_programs ) -from student.tests.factories import UserFactory, CourseModeFactory -from util.testing import EventTestMixin from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME +from util.testing import EventTestMixin +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, + ModuleStoreEnum, + SharedModuleStoreTestCase, +) from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, ModuleStoreEnum - -# These imports refer to lms djangoapps. -# Their testcases are only run under lms. -from bulk_email.models import Optout # pylint: disable=import-error -from certificates.models import CertificateStatuses # pylint: disable=import-error -from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification -import shoppingcart # pylint: disable=import-error -from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin - -# Explicitly import the cache from ConfigurationModel so we can reset it after each test -from config_models.models import cache log = logging.getLogger(__name__) @@ -889,276 +892,95 @@ 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 and don't mention XSeries! @attr(shard=3) +@httpretty.activate @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -@ddt.ddt -class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin): - """ - Tests for dashboard for xseries program courses. Enroll student into - programs and then try different combinations to see xseries upsell - messages are appearing. - """ +class RelatedProgramsTests(ProgramsApiConfigMixin, SharedModuleStoreTestCase): + """Tests verifying that related programs appear on the course dashboard.""" + url = None + maxDiff = None + password = 'test' + related_programs_preface = 'Related Programs' + + @classmethod + def setUpClass(cls): + super(RelatedProgramsTests, cls).setUpClass() + + cls.user = UserFactory() + cls.course = CourseFactory() + cls.enrollment = CourseEnrollmentFactory(user=cls.user, course_id=cls.course.id) # pylint: disable=no-member + ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) + + cls.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.programs = [ + programs_factories.Program( + organizations=[cls.organization], + course_codes=[course_code] + ) for __ in range(2) + ] + def setUp(self): - super(DashboardTestXSeriesPrograms, self).setUp() + super(RelatedProgramsTests, self).setUp() - self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test') - self.course_1 = CourseFactory.create() - self.course_2 = CourseFactory.create() - self.course_3 = CourseFactory.create() - self.program_name = 'Testing Program' - self.category = 'XSeries' + self.url = reverse('dashboard') - CourseModeFactory.create( - course_id=self.course_1.id, - mode_slug='verified', - mode_display_name='Verified', - expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1) + self.create_programs_config() + self.client.login(username=self.user.username, password=self.password) + + 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 assert_related_programs(self, response, are_programs_present=True): + """Assertion for verifying response contents.""" + assertion = getattr(self, 'assert{}Contains'.format('' if are_programs_present else 'Not')) + + for program in self.programs: + assertion(response, self.expected_link_text(program)) + + assertion(response, self.related_programs_preface) + + def expected_link_text(self, program): + """Construct expected dashboard link text.""" + return '{name} {category}'.format(name=program['name'], category=program['category']) + + def test_related_programs_listed(self): + """Verify that related programs are listed when the programs API returns data.""" + self.mock_programs_api(self.programs) + + response = self.client.get(self.url) + self.assert_related_programs(response) + + def test_no_data_no_programs(self): + """Verify that related programs aren't listed if the programs API returns no data.""" + self.mock_programs_api([]) + + response = self.client.get(self.url) + self.assert_related_programs(response, are_programs_present=False) + + def test_unrelated_program_not_listed(self): + """Verify that unrelated programs don't appear in the listing.""" + run_mode = programs_factories.RunMode(course_key='some/nonexistent/run') + course_code = programs_factories.CourseCode(run_modes=[run_mode]) + + unrelated_program = programs_factories.Program( + organizations=[self.organization], + course_codes=[course_code] ) - self.client = Client() - cache.clear() - def _create_program_data(self, data): - """Dry method to create testing programs data.""" - programs = {} - _id = 0 + self.mock_programs_api(self.programs + [unrelated_program]) - for course, program_status in data: - programs[unicode(course)] = [{ - 'id': _id, - 'category': self.category, - 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'}, - 'marketing_slug': 'fake-marketing-slug-xseries-1', - 'status': program_status, - 'course_codes': [ - { - 'display_name': 'Demo XSeries Program 1', - 'key': unicode(course), - 'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': unicode(course)}] - }, - { - 'display_name': 'Demo XSeries Program 2', - 'key': 'edx/demo/course_2', - 'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_2'}] - }, - { - 'display_name': 'Demo XSeries Program 3', - 'key': 'edx/demo/course_3', - 'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_3'}] - } - ], - 'subtitle': 'sub', - 'name': self.program_name - }] - - _id += 1 - - return programs - - @ddt.data( - ('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-1'), - ('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-2'), - ('active', [], ''), - ('unpublished', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-3'), - ) - @ddt.unpack - def test_get_xseries_programs_method(self, program_status, course_codes, marketing_slug): - """Verify that program data is parsed correctly for a given course""" - with patch('student.views.get_programs_for_dashboard') as mock_data: - mock_data.return_value = { - u'edx/demox/Run_1': [{ - 'id': 0, - 'category': self.category, - 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'}, - 'marketing_slug': marketing_slug, - 'status': program_status, - 'course_codes': course_codes, - 'subtitle': 'sub', - 'name': self.program_name - }] - } - parse_data = _get_course_programs( - self.user, [ - u'edx/demox/Run_1', u'valid/edX/Course' - ] - ) - - if program_status == 'unpublished': - self.assertEqual({}, parse_data) - else: - self.assertEqual( - { - u'edx/demox/Run_1': { - 'category': self.category, - 'course_program_list': [{ - 'program_id': 0, - 'course_count': len(course_codes), - 'display_name': self.program_name, - 'program_marketing_url': urljoin( - settings.MKTG_URLS.get('ROOT'), 'xseries' + '/{}' - ).format(marketing_slug) - }] - } - }, - parse_data - ) - - def test_program_courses_on_dashboard_without_configuration(self): - """If programs configuration is disabled then the xseries upsell messages - will not appear on student dashboard. - """ - CourseEnrollment.enroll(self.user, self.course_1.id) - self.client.login(username="jack", password="test") - with patch('student.views.get_programs_for_dashboard') as mock_method: - mock_method.return_value = self._create_program_data([]) - response = self.client.get(reverse('dashboard')) - - self.assertEquals(response.status_code, 200) - self.assertIn('Pursue a Certificate of Achievement to highlight', response.content) - self._assert_responses(response, 0) - - @ddt.data('verified', 'honor') - def test_modes_program_courses_on_dashboard_with_configuration(self, course_mode): - """Test that if program configuration is enabled than student can only - see those courses with xseries upsell messages which are active in - xseries programs. - """ - CourseEnrollment.enroll(self.user, self.course_1.id, mode=course_mode) - CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode) - - self.client.login(username="jack", password="test") - self.create_programs_config() - - with patch('student.views.get_programs_for_dashboard') as mock_data: - mock_data.return_value = self._create_program_data( - [(self.course_1.id, 'active'), (self.course_2.id, 'unpublished')] - ) - response = self.client.get(reverse('dashboard')) - # count total courses appearing on student dashboard - self.assertContains(response, 'course-container', 2) - self._assert_responses(response, 1) - - # for verified enrollment view the program detail button will have - # the class 'base-btn' - # for other modes view the program detail button will have have the - # class border-btn - if course_mode == 'verified': - self.assertIn('xseries-base-btn', response.content) - else: - self.assertIn('xseries-border-btn', response.content) - - @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) - @ddt.data((-2, -1), (-1, 1), (1, 2)) - @ddt.unpack - def test_start_end_offsets(self, start_days_offset, end_days_offset): - """Test that the xseries upsell messaging displays whether the course - has not yet started, is in session, or has already ended. - """ - self.course_1.start = datetime.now(pytz.UTC) + timedelta(days=start_days_offset) - self.course_1.end = datetime.now(pytz.UTC) + timedelta(days=end_days_offset) - self.update_course(self.course_1, self.user.id) - CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified') - - self.client.login(username="jack", password="test") - self.create_programs_config() - - with patch( - 'student.views.get_programs_for_dashboard', - return_value=self._create_program_data([(self.course_1.id, 'active')]) - ) as mock_get_programs: - response = self.client.get(reverse('dashboard')) - # ensure that our course id was included in the API call regardless of start/end dates - __, course_ids = mock_get_programs.call_args[0] - self.assertEqual(list(course_ids), [self.course_1.id]) - # count total courses appearing on student dashboard - self._assert_responses(response, 1) - - @ddt.data( - ('unpublished', 'unpublished', 'unpublished', 0), - ('active', 'unpublished', 'unpublished', 1), - ('active', 'active', 'unpublished', 2), - ('active', 'active', 'active', 3), - ) - @ddt.unpack - def test_different_programs_on_dashboard(self, status_1, status_2, status_3, program_count): - """Test the upsell on student dashboard with different programs - statuses. - """ - - CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified') - CourseEnrollment.enroll(self.user, self.course_2.id, mode='honor') - CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor') - - self.client.login(username="jack", password="test") - self.create_programs_config() - - with patch('student.views.get_programs_for_dashboard') as mock_data: - mock_data.return_value = self._create_program_data( - [(self.course_1.id, status_1), - (self.course_2.id, status_2), - (self.course_3.id, status_3)] - ) - - response = self.client.get(reverse('dashboard')) - # count total courses appearing on student dashboard - self.assertContains(response, 'course-container', 3) - self._assert_responses(response, program_count) - - @patch('student.views.log.warning') - @ddt.data('', 'course_codes', 'marketing_slug', 'name') - def test_program_courses_with_invalid_data(self, key_remove, log_warn): - """Test programs with invalid responses.""" - - CourseEnrollment.enroll(self.user, self.course_1.id) - self.client.login(username="jack", password="test") - self.create_programs_config() - - program_data = self._create_program_data([(self.course_1.id, 'active')]) - for program in program_data[unicode(self.course_1.id)]: - if key_remove and key_remove in program: - del program[key_remove] - - with patch('student.views.get_programs_for_dashboard') as mock_data: - mock_data.return_value = program_data - - response = self.client.get(reverse('dashboard')) - - # if data is invalid then warning log will be recorded. - if key_remove: - log_warn.assert_called_with( - 'Program structure is invalid, skipping display: %r', program_data[ - unicode(self.course_1.id) - ][0] - ) - # verify that no programs related upsell messages appear on the - # student dashboard. - self._assert_responses(response, 0) - else: - # in case of valid data all upsell messages will appear on dashboard. - self._assert_responses(response, 1) - - # verify that only normal courses (non-programs courses) appear on - # the student dashboard. - self.assertContains(response, 'course-container', 1) - self.assertIn('Pursue a Certificate of Achievement to highlight', response.content) - - def _assert_responses(self, response, count): - """Dry method to compare different programs related upsell messages, - classes. - """ - self.assertContains(response, 'label-xseries-association', count) - self.assertContains(response, 'btn xseries-', 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) + response = self.client.get(self.url) + self.assert_related_programs(response) + self.assertNotContains(response, unrelated_program['name']) class UserAttributeTests(TestCase): diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f9a0595e41..b9bb7ddfd7 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -120,8 +120,8 @@ 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 from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from openedx.core.djangoapps.programs import utils as programs_utils from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming import helpers as theming_helpers @@ -609,10 +609,11 @@ def dashboard(request): and has_access(request.user, 'view_courseware_with_prerequisites', enrollment.course_overview) ) - # Get any programs associated with courses being displayed. - # This is passed along in the template context to allow rendering of - # program-related information on the dashboard. - course_programs = _get_course_programs(user, [enrollment.course_id for enrollment in course_enrollments]) + # Find programs associated with courses being displayed. This information + # is passed in the template context to allow rendering of program-related + # information on the dashboard. + meter = programs_utils.ProgramProgressMeter(user, enrollments=course_enrollments) + programs_by_run = meter.engaged_programs(by_run=True) # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict @@ -736,9 +737,9 @@ def dashboard(request): 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, - 'course_programs': course_programs, - 'disable_courseware_js': True, + 'programs_by_run': programs_by_run, 'show_program_listing': ProgramsApiConfig.current().show_program_listing, + 'disable_courseware_js': True, } ecommerce_service = EcommerceService() @@ -2478,44 +2479,6 @@ def change_email_settings(request): return JsonResponse({"success": True}) -def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invalid-name - """Build a dictionary of program data required for display on the student dashboard. - - Given a user and an iterable of course keys, find all programs relevant to the - user and return them in a dictionary keyed by course key. - - Arguments: - user (User): The user to authenticate as when requesting programs. - user_enrolled_courses (list): List of course keys representing the courses in which - the given user has active enrollments. - - Returns: - dict, containing programs keyed by course. - """ - course_programs = get_programs_for_dashboard(user, user_enrolled_courses) - programs_data = {} - - for course_key, programs in course_programs.viewitems(): - for program in programs: - 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({ - 'course_count': len(program['course_codes']), - 'display_name': program['name'], - 'program_id': program['id'], - 'program_marketing_url': urljoin( - settings.MKTG_URLS.get('ROOT'), - 'xseries' + '/{}' - ).format(program['marketing_slug']) - }) - programs_for_course['category'] = program.get('category') - except KeyError: - log.warning('Program structure is invalid, skipping display: %r', program) - - return programs_data - - class LogoutView(TemplateView): """ Logs out user and redirects. diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 7f33442dde..d85d1d5506 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -1,7 +1,4 @@ """Learner dashboard views""" -from urlparse import urljoin - -from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.http import Http404 @@ -23,19 +20,13 @@ def program_listing(request): raise Http404 meter = utils.ProgramProgressMeter(request.user) - programs = meter.engaged_programs - - 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_url) context = { 'credentials': get_programs_credentials(request.user), 'disable_courseware_js': True, - 'marketing_url': marketing_url, + 'marketing_url': utils.get_program_marketing_url(programs_config), 'nav_hidden': True, - 'programs': programs, + 'programs': meter.engaged_programs(), 'progress': meter.progress, 'show_program_listing': programs_config.show_program_listing, 'uses_pattern_library': True, diff --git a/lms/static/js/dashboard/track_events.js b/lms/static/js/dashboard/track_events.js index bc78f2f641..6d5e0506c2 100644 --- a/lms/static/js/dashboard/track_events.js +++ b/lms/static/js/dashboard/track_events.js @@ -20,17 +20,6 @@ var edx = edx || {}; return properties; }; - // Generate object to be passed with programs events - edx.dashboard.generateProgramProperties = function(element) { - var $el = $(element); - - return { - category: 'dashboard', - course_id: $el.closest('.course-container').find('.info-course-id').html(), - program_id: $el.data('program-id') - }; - }; - // Emit an event when the 'course title link' is clicked. edx.dashboard.trackCourseTitleClicked = function($courseTitleLink, properties) { var trackProperty = properties || edx.dashboard.generateTrackProperties; @@ -92,24 +81,6 @@ var edx = edx || {}; ); }; - // Emit an event when the 'View XSeries Details' button is clicked - edx.dashboard.trackXseriesBtnClicked = function($xseriesBtn, properties) { - var trackProperty = properties || edx.dashboard.generateProgramProperties; - window.analytics.trackLink( - $xseriesBtn, - 'edx.bi.dashboard.xseries_cta_message.clicked', - trackProperty - ); - }; - - edx.dashboard.xseriesTrackMessages = function() { - $('.xseries-action .btn').each(function(i, element) { - var data = edx.dashboard.generateProgramProperties($(element)); - - window.analytics.track('edx.bi.dashboard.xseries_cta_message.viewed', data); - }); - }; - $(document).ready(function() { if (!window.analytics) { return; @@ -120,7 +91,5 @@ var edx = edx || {}; edx.dashboard.trackCourseOptionDropdownClicked($('.wrapper-action-more')); edx.dashboard.trackLearnVerifiedLinkClicked($('.verified-info')); edx.dashboard.trackFindCourseBtnClicked($('.btn-find-courses')); - edx.dashboard.trackXseriesBtnClicked($('.xseries-action .btn')); - edx.dashboard.xseriesTrackMessages(); }); })(jQuery); diff --git a/lms/static/js/fixtures/dashboard/dashboard.html b/lms/static/js/fixtures/dashboard/dashboard.html index 35f66cba43..2c93f85e75 100644 --- a/lms/static/js/fixtures/dashboard/dashboard.html +++ b/lms/static/js/fixtures/dashboard/dashboard.html @@ -27,10 +27,6 @@
-
- -

XSeries Program Course

-

Course details

@@ -96,32 +92,11 @@
-
-
-
-

XSeries Program: Interested in more courses in this subject?

-

-

- This course is 1 of 3 courses in the Water Management XSeries. -

-
- - - - View XSeries Details - -
-
-
- -

XSeries Program Course

-
-
-
-
-

XSeries Program: Interested in more courses in this subject?

-

- This course is 1 of 3 courses in the Water Management XSeries. -

-
- - - - View XSeries Details - -
-
diff --git a/lms/static/js/spec/dashboard/track_events_spec.js b/lms/static/js/spec/dashboard/track_events_spec.js index 48ce06ae53..72df81a5a9 100644 --- a/lms/static/js/spec/dashboard/track_events_spec.js +++ b/lms/static/js/spec/dashboard/track_events_spec.js @@ -92,31 +92,6 @@ property ); }); - - it('sends an analytics event when the user clicks the \'View XSeries Details\' button', function() { - var $xseries = $('.xseries-action .btn'); - window.edx.dashboard.trackXseriesBtnClicked( - $xseries, - window.edx.dashboard.generateProgramProperties); - - expect(window.analytics.trackLink).toHaveBeenCalledWith( - $xseries, - 'edx.bi.dashboard.xseries_cta_message.clicked', - window.edx.dashboard.generateProgramProperties - ); - }); - - it('sends an analytics event when xseries messages are present in the DOM on page load', function() { - window.edx.dashboard.xseriesTrackMessages(); - expect(window.analytics.track).toHaveBeenCalledWith( - 'edx.bi.dashboard.xseries_cta_message.viewed', - { - category: 'dashboard', - course_id: 'CTB3365DWx', - program_id: 'xseries007' - } - ); - }); }); }); }).call(this, window.define); diff --git a/lms/static/sass/_build-learner-dashboard.scss b/lms/static/sass/_build-learner-dashboard.scss index b7d41431ee..e37c4dadf6 100644 --- a/lms/static/sass/_build-learner-dashboard.scss +++ b/lms/static/sass/_build-learner-dashboard.scss @@ -3,8 +3,8 @@ // Uses the Pattern Library @import 'elements/banners'; -@import 'elements/program-card'; @import 'elements/course-card'; -@import 'elements/icons'; -@import 'views/program-list'; +@import 'elements/program-card'; +@import 'elements-v2/icons'; @import 'views/program-details'; +@import 'views/program-list'; diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index 5a4d609076..bed818540f 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -13,11 +13,12 @@ @import 'base/base'; // base - elements -@import 'elements/typography'; @import 'elements/controls'; +@import 'elements/creative-commons'; +@import 'elements/icons'; @import 'elements/navigation'; @import 'elements/pagination'; -@import 'elements/creative-commons'; +@import 'elements/typography'; // shared - course @import 'shared/fields'; diff --git a/lms/static/sass/elements-v2/_icons.scss b/lms/static/sass/elements-v2/_icons.scss new file mode 100644 index 0000000000..cdbddd4ada --- /dev/null +++ b/lms/static/sass/elements-v2/_icons.scss @@ -0,0 +1,34 @@ +.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; +} + +.certificate-body { + // Use the ampersand to reference parent selectors. + .certificate-icon & { + @include float(left); + @include margin-right($baseline*0.4); + + margin-top: ($baseline/10); + width: 23px; + height: 20px; + padding: 2px; + background-color: $white; + border-style: solid; + border-width: 2px; + } + + .green-icon & { + fill: palette(success, text); + border-color: palette(success, text); + } + + .blue-icon & { + fill: palette(primary, dark); + border-color: palette(primary, dark); + } +} diff --git a/lms/static/sass/elements/_icons.scss b/lms/static/sass/elements/_icons.scss index 10d07773c3..67dac4b611 100644 --- a/lms/static/sass/elements/_icons.scss +++ b/lms/static/sass/elements/_icons.scss @@ -1,22 +1,8 @@ -.certificate-icon .certificate-body { - @include float(left); - @include margin-right($baseline*0.4); - - margin-top: ($baseline/10); - width: 23px; - height: 20px; - padding: 2px; - background-color: $white; - border-style: solid; - border-width: 2px; +.xseries-icon { + background: url('#{$static-path}/images/programs/xseries-icon.svg') no-repeat; } -.green-certificate-icon .certificate-body { - fill: palette(success, accent); - border-color: palette(success, accent); -} - -.blue-certificate-icon .certificate-body { - fill: palette(primary, dark); - border-color: palette(primary, dark); +.micromasters-icon { + margin-top: $baseline * 0.05; + background: url('#{$static-path}/images/programs/micromasters-icon.svg') no-repeat; } diff --git a/lms/static/sass/elements/_program-card.scss b/lms/static/sass/elements/_program-card.scss index b619cdc04e..366bb471fb 100644 --- a/lms/static/sass/elements/_program-card.scss +++ b/lms/static/sass/elements/_program-card.scss @@ -97,15 +97,6 @@ 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 { diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index b5885236f5..308356f5be 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -62,32 +62,6 @@ } } - .wrapper-xseries-certificates{ - @include float(right); - @include margin-left(flex-gutter()); - width: flex-grid(3); - - .title{ - @extend %t-title7; - @extend %t-weight4; - } - - ul{ - @include padding-left(0); - margin-top: ($baseline/2); - } - - li{ - @include line-height(20); - list-style-type: none; - } - - .copy { - @extend %t-copy-sub1; - margin-top: ($baseline/2); - } - } - .profile-sidebar { background: transparent; @include float(right); @@ -304,31 +278,11 @@ border-bottom: 4px solid $border-color-l4; padding-bottom: $baseline; - .course-container{ + .course-container { border: 1px solid $border-color-l4; border-radius: 3px; - - // CASE: Xseries associated course - .label-xseries-association{ - @include margin($baseline/2, $baseline/5, 0, $baseline/2); - - .xseries-icon{ - @include float(left); - - @include margin-right($baseline*0.4); - background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat; - background-color: transparent; - - width: ($baseline*1.1); - height: ($baseline*1.1); - } - - .message-copy{ - padding-top: ($baseline/5); - @extend %t-action3; - } - } } + &:last-child { margin-bottom: 0; border-bottom: none; @@ -860,100 +814,6 @@ } } - .xseries-action{ - .xseries-msg{ - @include float(left); - width: flex-grid(9, 12); - } - - .message-copy{ - @extend %t-demi-strong; - margin-top: 0; - } - - .message-copy-bold{ - @extend %t-strong; - } - - .xseries-border-btn { - @extend %btn-pl-black-border; - @include float(right); - position: relative; - @include left(10px); - padding: ($baseline*0.4) ($baseline*0.6); - background-image: none ; - text-shadow: none; - box-shadow: none; - text-transform: none; - - .action-xseries-icon{ - @include float(left); - display: inline; - - @include margin-right($baseline*0.4); - background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat; - background-color: transparent; - - width: ($baseline*1.1); - height: ($baseline*1.1); - } - &:hover, - &:focus { - - .action-xseries-icon{ - @include float(left); - display: inline; - - @include margin-right($baseline*0.4); - background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat; - background-color: transparent; - - width: ($baseline*1.1); - height: ($baseline*1.1); - } - } - } - - .xseries-base-btn { - @extend %btn-pl-black-base; - @include float(right); - position: relative; - @include left(10px); - padding: ($baseline*0.4) ($baseline*0.6); - background-image: none ; - text-shadow: none; - box-shadow: none; - text-transform: none; - - .action-xseries-icon{ - @include float(left); - display: inline; - - @include margin-right($baseline*0.4); - background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat; - background-color: transparent; - - width: ($baseline*1.1); - height: ($baseline*1.1); - } - &:hover, - &:focus { - - .action-xseries-icon { - @include float(left); - display: inline; - - @include margin-right($baseline*0.4); - background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat; - background-color: transparent; - - width: ($baseline*1.1); - height: ($baseline*1.1); - } - } - } - } - .actions { .action { @@ -1129,6 +989,46 @@ } } + &.message-related-programs { + background: none; + border: none; + margin-top: ($baseline/4); + padding-bottom: 0; + + .related-programs-preface { + @include float(left); + font-weight: bold; + } + + ul { + display: inline; + padding: 0; + margin: 0; + } + + li { + @include float(left); + display: inline; + padding: 0 0.5em; + border-right: 1px solid; + + .category-icon { + @include float(left); + @include margin-right($baseline/4); + margin-top: ($baseline/10); + background-color: transparent; + background-size: 100%; + width: ($baseline*0.7); + height: ($baseline*0.7); + } + } + + // Remove separator from last list item. + li:last-child { + border: 0; + } + } + // TYPE: pre-requisites .prerequisites { @include clearfix; diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 4cc0be1f36..1beaa56c4a 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -98,8 +98,8 @@ from openedx.core.djangolib.markup import HTML, Text <% is_course_blocked = (enrollment.course_id in block_courses) %> <% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %> <% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %> - <% course_program_info = course_programs.get(unicode(enrollment.course_id)) %> - <%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info" /> + <% related_programs = programs_by_run.get(unicode(enrollment.course_id)) %> + <%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs" /> % endfor diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 1904dfc2ac..0bf4d6d1c5 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -1,4 +1,4 @@ -<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, course_program_info" expression_filter="h"/> +<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs" expression_filter="h"/> <%! import urllib @@ -53,12 +53,6 @@ from student.helpers import ( <% mode_class = '' %> % endif
- % if course_program_info and course_program_info.get('category')=='XSeries': -
- -

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

-
- % endif
<% course_target = reverse('info', args=[unicode(course_overview.id)]) %>
@@ -283,6 +277,20 @@ from student.helpers import (