diff --git a/lms/djangoapps/learner_dashboard/programs.py b/lms/djangoapps/learner_dashboard/programs.py index 5848fefa39..4374d02cea 100644 --- a/lms/djangoapps/learner_dashboard/programs.py +++ b/lms/djangoapps/learner_dashboard/programs.py @@ -32,7 +32,11 @@ class ProgramsFragmentView(EdxFragmentView): Render the program listing fragment. """ user = request.user - mobile_only = json.loads(request.GET.get('mobile_only', 'false')) + try: + mobile_only = json.loads(request.GET.get('mobile_only', 'false')) + except ValueError: + mobile_only = False + programs_config = kwargs.get('programs_config') or ProgramsApiConfig.current() if not programs_config.enabled or not user.is_authenticated(): raise Http404 @@ -80,7 +84,12 @@ class ProgramDetailsFragmentView(EdxFragmentView): if not program_data: raise Http404 - program_data = ProgramDataExtender(program_data, request.user).extend() + try: + mobile_only = json.loads(request.GET.get('mobile_only', 'false')) + except ValueError: + mobile_only = False + + program_data = ProgramDataExtender(program_data, request.user, mobile_only=mobile_only).extend() course_data = meter.progress(programs=[program_data], count_only=False)[0] certificate_data = get_certificates(request.user, program_data) diff --git a/lms/static/js/learner_dashboard/models/course_card_model.js b/lms/static/js/learner_dashboard/models/course_card_model.js index 330854f36f..e8459973b5 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -56,7 +56,9 @@ $.extend(unselectedRun, { marketing_url: courseRun.marketing_url, - is_enrollment_open: courseRun.is_enrollment_open + is_enrollment_open: courseRun.is_enrollment_open, + key: courseRun.key || '', + is_mobile_only: courseRun.is_mobile_only || false }); } @@ -196,7 +198,7 @@ if (isEnrolled && courseRun.course_url) { courseTitleLink = courseRun.course_url; } else if (!isEnrolled && courseRun.marketing_url) { - courseTitleLink = courseRun.marketing_url; + courseTitleLink = this.updateMarketingUrl(courseRun); } this.set({ certificate_url: courseRun.certificate_url, @@ -217,7 +219,8 @@ upcoming_course_runs: this.getUpcomingCourseRuns(), upgrade_url: courseRun.upgrade_url, price: this.getCertificatePriceString(courseRun), - course_title_link: courseTitleLink + course_title_link: courseTitleLink, + is_mobile_only: courseRun.is_mobile_only || false }); // This is used to render the date for completed and in progress courses @@ -240,6 +243,30 @@ }); this.setActiveCourseRun(selectedCourseRun); } + }, + + // update marketing url for deep linking if is_mobile_only true + updateMarketingUrl: function(courseRun) { + if (courseRun.is_mobile_only === true) { + var marketingUrl = courseRun.marketing_url, // eslint-disable-line vars-on-top + href = marketingUrl, + path, + start; + + if (marketingUrl.indexOf('course_info?path_id') < 0) { + start = marketingUrl.indexOf('course/'); + + if (start > -1) { + path = marketingUrl.substr(start); + } + + href = 'edxapp://course_info?path_id=' + path; + } + + return href; + } else { + return courseRun.marketing_url; + } } }); }); diff --git a/lms/static/js/learner_dashboard/views/course_enroll_view.js b/lms/static/js/learner_dashboard/views/course_enroll_view.js index c41bc280ae..e0d5ecfb7f 100644 --- a/lms/static/js/learner_dashboard/views/course_enroll_view.js +++ b/lms/static/js/learner_dashboard/views/course_enroll_view.js @@ -22,7 +22,8 @@ tpl: HtmlUtils.template(pageTpl), events: { - 'click .enroll-button': 'handleEnroll' + 'click .enroll-button': 'handleEnroll', + 'change .run-select': 'updateEnrollUrl' }, initialize: function(options) { @@ -55,16 +56,18 @@ handleEnroll: function() { // Enrollment click event handled here - var courseRunKey = $('.run-select').val() || this.model.get('course_run_key'); - this.model.updateCourseRun(courseRunKey); - if (this.model.get('is_enrolled')) { - // Create the enrollment. - this.enrollModel.save({ - course_id: courseRunKey - }, { - success: _.bind(this.enrollSuccess, this), - error: _.bind(this.enrollError, this) - }); + if (this.model.get('is_mobile_only') !== true) { + var courseRunKey = $('.run-select').val() || this.model.get('course_run_key'); // eslint-disable-line vars-on-top, max-len + this.model.updateCourseRun(courseRunKey); + if (this.model.get('is_enrolled')) { + // Create the enrollment. + this.enrollModel.save({ + course_id: courseRunKey + }, { + success: _.bind(this.enrollSuccess, this), + error: _.bind(this.enrollError, this) + }); + } } }, @@ -100,6 +103,14 @@ } }, + updateEnrollUrl: function() { + if (this.model.get('is_mobile_only') === true) { + var courseRunKey = $('.run-select').val(), // eslint-disable-line vars-on-top + href = 'edxapp://enroll?course_id=' + courseRunKey + '&email_opt_in=true'; + $('.enroll-course-button').attr('href', href); + } + }, + redirect: function(url) { window.location.href = url; } diff --git a/lms/static/sass/views/_program-details.scss b/lms/static/sass/views/_program-details.scss index 76a13a9d82..a9be0be568 100644 --- a/lms/static/sass/views/_program-details.scss +++ b/lms/static/sass/views/_program-details.scss @@ -352,7 +352,7 @@ margin-bottom: 0; } - button { + button, .enroll-course-button { background-color: palette(primary, dark); height: 37px; width: 128px; @@ -371,6 +371,10 @@ @include float(right); } } + + .enroll-course-button { + padding: 7px 18.5px 0 18.5px; + } } .select-choice { diff --git a/lms/templates/learner_dashboard/course_enroll.underscore b/lms/templates/learner_dashboard/course_enroll.underscore index 0038abfb2f..99d079ec97 100644 --- a/lms/templates/learner_dashboard/course_enroll.underscore +++ b/lms/templates/learner_dashboard/course_enroll.underscore @@ -33,9 +33,15 @@ <% } %>
<% } else if (upcoming_course_runs.length > 0) {%> <% } %> - <% if (is_learner_eligible_for_one_click_purchase) { %> + <% if (is_learner_eligible_for_one_click_purchase && (typeof is_mobile_only === 'undefined' || is_mobile_only === false)) { %> <%- gettext('Upgrade All Remaining Courses (')%> <% if (discount_data.is_discounted) { %> diff --git a/lms/templates/learner_dashboard/upgrade_message.underscore b/lms/templates/learner_dashboard/upgrade_message.underscore index 6d79a51238..981c29b7f7 100644 --- a/lms/templates/learner_dashboard/upgrade_message.underscore +++ b/lms/templates/learner_dashboard/upgrade_message.underscore @@ -3,8 +3,10 @@ <%- gettext('Needs verified certificate ') %> <%- price %> +<% if (typeof is_mobile_only === 'undefined' || is_mobile_only === false) { %> +<% } %> diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index b123f02a45..6a22a237c5 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -3,6 +3,7 @@ import datetime import json import uuid +from copy import deepcopy import ddt import httpretty @@ -823,7 +824,7 @@ class TestProgramDataExtender(ModuleStoreTestCase): """Tests of the program data extender utility class.""" maxDiff = None sku = 'abc123' - checkout_path = '/basket' + checkout_path = '/basket/add' instructors = { 'instructors': [ { @@ -852,7 +853,11 @@ class TestProgramDataExtender(ModuleStoreTestCase): def _assert_supplemented(self, actual, **kwargs): """DRY helper used to verify that program data is extended correctly.""" - self.course_run.update( + program = deepcopy(self.program) + course_run = deepcopy(self.course_run) + course = deepcopy(self.catalog_course) + + course_run.update( dict( { 'certificate_url': None, @@ -868,10 +873,10 @@ class TestProgramDataExtender(ModuleStoreTestCase): ) ) - self.catalog_course['course_runs'] = [self.course_run] - self.program['courses'] = [self.catalog_course] + course['course_runs'] = [course_run] + program['courses'] = [course] - self.assertEqual(actual, self.program) + self.assertEqual(actual, program) @ddt.data(-1, 0, 1) def test_is_enrollment_open(self, days_offset): @@ -887,15 +892,13 @@ class TestProgramDataExtender(ModuleStoreTestCase): self._assert_supplemented(data) @ddt.data( - (False, None, False), (True, MODES.audit, True), - (True, MODES.verified, False), ) @ddt.unpack @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode): """Verify that program data is supplemented with the student's enrollment status.""" - expected_upgrade_url = '{root}/{path}?sku={sku}'.format( + expected_upgrade_url = '{root}/{path}/?sku={sku}'.format( root=ECOMMERCE_URL_ROOT, path=self.checkout_path.strip('/'), sku=self.sku, @@ -1275,6 +1278,14 @@ class TestProgramDataExtender(ModuleStoreTestCase): self.assertTrue(data['is_learner_eligible_for_one_click_purchase']) self.assertEqual(set(data['skus']), expected_skus) + def test_course_url_with_mobile_only(self): + """ + Verify that correct course url is returned for mobile. + """ + data = ProgramDataExtender(self.program, self.user, mobile_only=True).extend() + expected_course_url = 'edxapp://enrolled_course_info?course_id={}'.format(self.course.id) + self._assert_supplemented(data, course_url=expected_course_url) + @skip_unless_lms @mock.patch(UTILS_MODULE + '.get_credentials') diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index ccae2ddaa2..3c9457c329 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -82,6 +82,7 @@ class ProgramProgressMeter(object): def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False): self.site = site self.user = user + self.mobile_only = mobile_only self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) @@ -106,7 +107,7 @@ class ProgramProgressMeter(object): if uuid: self.programs = [get_programs(self.site, uuid=uuid)] else: - self.programs = attach_program_detail_url(get_programs(self.site), mobile_only) + self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only) def invert_programs(self): """Intersect programs and enrollments. @@ -407,9 +408,11 @@ class ProgramDataExtender(object): program_data (dict): Representation of a program. user (User): The user whose enrollments to inspect. """ - def __init__(self, program_data, user): + def __init__(self, program_data, user, mobile_only=False): self.data = program_data self.user = user + self.mobile_only = mobile_only + self.data.update({'is_mobile_only': self.mobile_only}) self.course_run_key = None self.course_overview = None @@ -452,7 +455,10 @@ class ProgramDataExtender(object): ) if certificate_uuid else None def _attach_course_run_course_url(self, run_mode): - run_mode['course_url'] = reverse('course_root', args=[self.course_run_key]) + if self.mobile_only: + run_mode['course_url'] = 'edxapp://enrolled_course_info?course_id={}'.format(run_mode.get('key')) + else: + run_mode['course_url'] = reverse('course_root', args=[self.course_run_key]) def _attach_course_run_enrollment_open_date(self, run_mode): run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE') @@ -498,6 +504,9 @@ class ProgramDataExtender(object): def _attach_course_run_may_certify(self, run_mode): run_mode['may_certify'] = self.course_overview.may_certify() + def _attach_course_run_is_mobile_only(self, run_mode): + run_mode['is_mobile_only'] = self.mobile_only + def _filter_out_courses_with_entitlements(self, courses): """ Removes courses for which the current user already holds an applicable entitlement. @@ -670,7 +679,6 @@ def get_certificates(user, extended_program): return certificates -# pylint: disable=missing-docstring class ProgramMarketingDataExtender(ProgramDataExtender): """ Utility for extending program data meant for the program marketing page which lives in the