From e966188ba63fbbba83b37d2e6874e35c80760f5c Mon Sep 17 00:00:00 2001 From: Adeel Khan Date: Wed, 19 Jul 2017 05:50:23 +0500 Subject: [PATCH] Improve ECOM connection on student dashboard via on demand ajax calls. Fix course unenroll dialog's incorrect message rendering. LEARNER-2033 --- .../djangoapps/student/tests/test_refunds.py | 20 --- common/djangoapps/student/tests/test_views.py | 27 +++- common/djangoapps/student/urls.py | 3 + common/djangoapps/student/views.py | 38 ++++-- common/test/acceptance/pages/lms/dashboard.py | 41 +++++- .../tests/lms/test_lms_dashboard.py | 62 +++++++++- lms/static/js/dashboard/legacy.js | 117 ++++++++++++++---- lms/templates/dashboard.html | 3 +- .../dashboard/_dashboard_course_listing.html | 110 +++++----------- themes/edx.org/lms/templates/dashboard.html | 3 +- 10 files changed, 285 insertions(+), 139 deletions(-) diff --git a/common/djangoapps/student/tests/test_refunds.py b/common/djangoapps/student/tests/test_refunds.py index 11849cbd0a..8ef2e3de74 100644 --- a/common/djangoapps/student/tests/test_refunds.py +++ b/common/djangoapps/student/tests/test_refunds.py @@ -77,26 +77,6 @@ class RefundableTest(SharedModuleStoreTestCase): self.verified_mode.save() self.assertTrue(self.enrollment.refundable()) - def test_refundable_of_purchased_course(self): - """ Assert that courses without a verified mode are not refundable""" - self.client.login(username=self.user.username, password=self.USER_PASSWORD) - course = CourseFactory.create() - CourseModeFactory.create( - course_id=course.id, - mode_slug='honor', - min_price=10, - currency='usd', - mode_display_name='honor', - expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1) - ) - enrollment = CourseEnrollment.enroll(self.user, course.id, mode='honor') - - # TODO: Until we can allow course administrators to define a refund period for paid for courses show_refund_option should be False. # pylint: disable=fixme - self.assertFalse(enrollment.refundable()) - - resp = self.client.post(reverse('student.views.dashboard', args=[])) - self.assertIn('You will not be refunded the amount you paid.', resp.content) - @patch('student.models.CourseEnrollment.refund_cutoff_date') def test_refundable_when_certificate_exists(self, cutoff_date): """ Assert that enrollment is not refundable once a certificat has been generated.""" diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index a5cd234665..c74b5e690d 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -15,6 +15,7 @@ from edx_oauth2_provider.constants import AUTHORIZED_CLIENTS_SESSION_KEY from edx_oauth2_provider.tests.factories import ClientFactory, TrustedClientFactory from mock import patch from pyquery import PyQuery as pq +from opaque_keys import InvalidKeyError from student.cookies import get_user_info_cookie_data from student.helpers import DISABLE_UNENROLL_CERT_STATES @@ -44,7 +45,7 @@ class TestStudentDashboardUnenrollments(SharedModuleStoreTestCase): """ Create a course and user, then log in. """ super(TestStudentDashboardUnenrollments, self).setUp() self.user = UserFactory() - CourseEnrollmentFactory(course_id=self.course.id, user=self.user) + self.enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) self.cert_status = None self.client.login(username=self.user.username, password=PASSWORD) @@ -119,6 +120,30 @@ class TestStudentDashboardUnenrollments(SharedModuleStoreTestCase): self.assertEqual(response.status_code, 200) + def test_course_run_refund_status_successful(self): + """ Assert that view:course_run_refund_status returns correct Json for successful refund call.""" + with patch('student.models.CourseEnrollment.refundable', return_value=True): + response = self.client.get(reverse('course_run_refund_status', kwargs={'course_id': self.course.id})) + + self.assertEquals(json.loads(response.content), {'course_refundable_status': True}) + self.assertEqual(response.status_code, 200) + + with patch('student.models.CourseEnrollment.refundable', return_value=False): + response = self.client.get(reverse('course_run_refund_status', kwargs={'course_id': self.course.id})) + + self.assertEquals(json.loads(response.content), {'course_refundable_status': False}) + self.assertEqual(response.status_code, 200) + + def test_course_run_refund_status_invalid_course_key(self): + """ Assert that view:course_run_refund_status returns correct Json for Invalid Course Key .""" + with patch('opaque_keys.edx.keys.CourseKey.from_string') as mock_method: + mock_method.side_effect = InvalidKeyError('CourseKey', 'The course key used to get refund status caused \ + InvalidKeyError during look up.') + response = self.client.get(reverse('course_run_refund_status', kwargs={'course_id': self.course.id})) + + self.assertEquals(json.loads(response.content), {'course_refundable_status': ''}) + self.assertEqual(response.status_code, 406) + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class LogoutTests(TestCase): diff --git a/common/djangoapps/student/urls.py b/common/djangoapps/student/urls.py index 269e506150..6dc4e61cb2 100644 --- a/common/djangoapps/student/urls.py +++ b/common/djangoapps/student/urls.py @@ -38,6 +38,9 @@ urlpatterns = ( 'password_reset_confirm_wrapper', name='password_reset_confirm', ), + url(r'^course_run/{}/refund_status$'.format(settings.COURSE_ID_PATTERN), + 'course_run_refund_status', + name="course_run_refund_status"), ) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 0c24bbfbb6..ddbbc837bf 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -788,14 +788,6 @@ def dashboard(request): statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) - user_already_has_certs_for = GeneratedCertificate.course_ids_with_certs_for_user(request.user) - show_refund_option_for = frozenset( - enrollment.course_id for enrollment in course_enrollments - if enrollment.refundable( - user_already_has_certs_for=user_already_has_certs_for - ) - ) - block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( @@ -861,7 +853,6 @@ def dashboard(request): 'verification_status': verification_status, 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, - 'show_refund_option_for': show_refund_option_for, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, @@ -892,6 +883,35 @@ def dashboard(request): return response +@login_required +def course_run_refund_status(request, course_id): + """ + Get Refundable status for a course. + + Arguments: + request: The request object. + course_id (str): The unique identifier for the course. + + Returns: + Json response. + + """ + + try: + course_key = CourseKey.from_string(course_id) + course_enrollment = CourseEnrollment.get_enrollment(request.user, course_key) + + except InvalidKeyError: + logging.exception("The course key used to get refund status caused InvalidKeyError during look up.") + + return JsonResponse({'course_refundable_status': ''}, status=406) + + refundable_status = course_enrollment.refundable() + logging.info("Course refund status for course {0} is {1}".format(course_id, refundable_status)) + + return JsonResponse({'course_refundable_status': refundable_status}, status=200) + + def get_verification_error_reasons_for_display(verification_error_codes): verification_errors = [] verification_error_map = { diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index 5eadc3ec7a..24dbb07caf 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -135,11 +135,50 @@ class DashboardPage(PageObject): else: return None + def view_course_unenroll_dialog_message(self, course_id): + """ + Go to the course unenroll dialog message for `course_id` (e.g. edx/Open_DemoX/edx_demo_course) + """ + div_index = self.get_course_actions_link_css(course_id) + button_link_css = "#actions-dropdown-link-{}".format(div_index) + unenroll_css = "#unenroll-{}".format(div_index) + + if button_link_css is not None: + self.q(css=button_link_css).first.click() + self.wait_for_element_visibility(unenroll_css, 'Unenroll message dialog is visible.') + self.q(css=unenroll_css).first.click() + self.wait_for_ajax() + + return { + 'track-info': self.q(css='#track-info').html, + 'refund-info': self.q(css='#refund-info').html + } + + else: + msg = "No links found for course {0}".format(course_id) + self.warning(msg) + + def get_course_actions_link_css(self, course_id): + """ + Return a index for unenroll button with `course_id`. + """ + # Get the link hrefs for all courses + all_divs = self.q(css='div.wrapper-action-more').map(lambda el: el.get_attribute('data-course-key')).results + + # Search for the first link that matches the course id + div_index = None + for index in range(len(all_divs)): + if course_id in all_divs[index]: + div_index = index + break + + return div_index + def pre_requisite_message_displayed(self): """ Verify if pre-requisite course messages are being displayed. """ - return self.q(css='li.prerequisites > .tip').visible + return self.q(css='div.prerequisites > .tip').visible def get_course_listings(self): """Retrieve the list of course DOM elements""" diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard.py b/common/test/acceptance/tests/lms/test_lms_dashboard.py index 62c3562f72..7ea1b3f348 100644 --- a/common/test/acceptance/tests/lms/test_lms_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_dashboard.py @@ -76,19 +76,25 @@ class BaseLmsDashboardTestMultiple(UniqueCourseTest): 'org': 'test_org', 'number': self.unique_id, 'run': 'test_run_A', - 'display_name': 'Test Course A' + 'display_name': 'Test Course A', + 'enrollment_mode': 'audit', + 'cert_name_long': 'Certificate of Audit Achievement' }, 'B': { 'org': 'test_org', 'number': self.unique_id, 'run': 'test_run_B', - 'display_name': 'Test Course B' + 'display_name': 'Test Course B', + 'enrollment_mode': 'verified', + 'cert_name_long': 'Certificate of Verified Achievement' }, 'C': { 'org': 'test_org', 'number': self.unique_id, 'run': 'test_run_C', - 'display_name': 'Test Course C' + 'display_name': 'Test Course C', + 'enrollment_mode': 'credit', + 'cert_name_long': 'Certificate of Credit Achievement' } } @@ -113,7 +119,8 @@ class BaseLmsDashboardTestMultiple(UniqueCourseTest): ) course_fixture.add_advanced_settings({ - u"social_sharing_url": {u"value": "http://custom/course/url"} + u"social_sharing_url": {u"value": "http://custom/course/url"}, + u"cert_name_long": {u"value": value['cert_name_long']} }) course_fixture.install() @@ -126,7 +133,8 @@ class BaseLmsDashboardTestMultiple(UniqueCourseTest): self.browser, username=self.username, email=self.email, - course_id=course_key + course_id=course_key, + enrollment_mode=value['enrollment_mode'] ).visit() # Navigate the authenticated, enrolled user to the dashboard page and get testing! @@ -346,6 +354,50 @@ class LmsDashboardPageTest(BaseLmsDashboardTest): self.assertEqual(profile_img.attrs('alt')[0], '') +class LmsDashboardCourseUnEnrollDialogMessageTest(BaseLmsDashboardTestMultiple): + """ + Class to test lms student dashboard unenroll dialog messages. + """ + + def test_audit_course_run_unenroll_dialog_msg(self): + """ + Validate unenroll dialog message when user clicks unenroll button for a audit course + """ + + self.dashboard_page.visit() + dialog_message = self.dashboard_page.view_course_unenroll_dialog_message(str(self.course_keys['A'])) + course_number = self.courses['A']['number'] + course_name = self.courses['A']['display_name'] + + expected_track_message = u'Are you sure you want to unenroll from' + \ + u' ' + course_name + u'' + \ + u' (' + course_number + u')?' + + self.assertEqual(dialog_message['track-info'][0], expected_track_message) + + def test_verified_course_run_unenroll_dialog_msg(self): + """ + Validate unenroll dialog message when user clicks unenroll button for a verified course passed refund + deadline + """ + + self.dashboard_page.visit() + dialog_message = self.dashboard_page.view_course_unenroll_dialog_message(str(self.course_keys['B'])) + course_number = self.courses['B']['number'] + course_name = self.courses['B']['display_name'] + cert_long_name = self.courses['B']['cert_name_long'] + + expected_track_message = u'Are you sure you want to unenroll from the verified' + \ + u' ' + cert_long_name + u'' + \ + u' track of ' + course_name + u'' + \ + u' (' + course_number + u')?' + + expected_refund_message = u'The refund deadline for this course has passed,so you will not receive a refund.' + + self.assertEqual(dialog_message['track-info'][0], expected_track_message) + self.assertEqual(dialog_message['refund-info'][0], expected_refund_message) + + @attr('a11y') class LmsDashboardA11yTest(BaseLmsDashboardTestMultiple): """ diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index 233859e001..27111c4576 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -79,6 +79,35 @@ return properties; } + function setDialogAttributes(isPaidCourse, certNameLong, + courseNumber, courseName, enrollmentMode, showRefundOption) { + var diagAttr = {}; + + if (isPaidCourse) { + if (showRefundOption) { + diagAttr['data-refund-info'] = gettext('You will be refunded the amount you paid.'); + } else { + diagAttr['data-refund-info'] = gettext('You will not be refunded the amount you paid.'); + } + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the purchased course ' + + '%(courseName)s (%(courseNumber)s)?'); + } else if (enrollmentMode !== 'verified') { + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from %(courseName)s ' + + '(%(courseNumber)s)?'); + } else if (showRefundOption) { + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' + + '%(certNameLong)s track of %(courseName)s (%(courseNumber)s)?'); + diagAttr['data-refund-info'] = gettext('You will be refunded the amount you paid.'); + } else { + diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' + + '%(certNameLong)s track of %(courseName)s (%(courseNumber)s)?'); + diagAttr['data-refund-info'] = gettext('The refund deadline for this course has passed,' + + 'so you will not receive a refund.'); + } + + return diagAttr; + } + $('#failed-verification-button-dismiss').click(function() { $.ajax({ url: urls.verifyToggleBannerFailedOff, @@ -95,29 +124,70 @@ }); $('.action-email-settings').click(function(event) { - var element = $(event.target); - $('#email_settings_course_id').val(element.data('course-id')); - $('#email_settings_course_number').text(element.data('course-number')); + $('#email_settings_course_id').val($(event.target).data('course-id')); + $('#email_settings_course_number').text($(event.target).data('course-number')); if ($(event.target).data('optout') === 'False') { $('#receive_emails').prop('checked', true); } edx.dashboard.dropdown.toggleCourseActionsDropdownMenu(event); }); - $('.action-unenroll').click(function(event) { - var element = $(event.target); - var track_info = element.data('track-info'); - var course_number = element.data('course-number'); - var course_name = element.data('course-name'); - var cert_name_long = element.data('cert-name-long'); - $('#track-info').html(interpolate(track_info, { - course_number: "" + course_number + '', - course_name: "" + course_name + '', - cert_name_long: "" + cert_name_long + '' - }, true)); - $('#refund-info').html(element.data('refund-info')); - $('#unenroll_course_id').val(element.data('course-id')); - edx.dashboard.dropdown.toggleCourseActionsDropdownMenu(event); + var isPaidCourse = $(event.target).data('course-is-paid-course') === 'True'; + var certNameLong = $(event.target).data('course-cert-name-long'); + var enrollmentMode = $(event.target).data('course-enrollment-mode'); + + var courseNumber = $(event.target).data('course-number'); + var courseName = $(event.target).data('course-name'); + var courseRefundUrl = $(event.target).data('course-refund-url'); + var dialogMessageAttr; + + var request = $.ajax({ + url: courseRefundUrl, + method: 'GET', + dataType: 'json' + }); + request.success(function(data, textStatus, xhr) { + if (xhr.status === 200) { + dialogMessageAttr = setDialogAttributes(isPaidCourse, certNameLong, + courseNumber, courseName, enrollmentMode, data.course_refundable_status); + + $('#track-info').empty(); + $('#refund-info').empty(); + + $('#track-info').html(interpolate(dialogMessageAttr['data-track-info'], { + courseNumber: ['', courseNumber, ''].join(''), + courseName: ['', courseName, ''].join(''), + certNameLong: ['', certNameLong, ''].join('') + }, true)); + + + if ('data-refund-info' in dialogMessageAttr) { + $('#refund-info').text(dialogMessageAttr['data-refund-info']); + } + + $('#unenroll_course_id').val($(event.target).data('course-id')); + } else { + $('#unenroll_error').text( + gettext('Unable to determine whether we should give you a refund because' + + ' of System Error. Please try again later.') + ).stop() + .css('display', 'block'); + + $('#unenroll_form input[type="submit"]').prop('disabled', true); + } + edx.dashboard.dropdown.toggleCourseActionsDropdownMenu(event); + }); + request.fail(function() { + $('#unenroll_error').text( + gettext('Unable to determine whether we should give you a refund because' + + ' of System Error. Please try again later.') + ).stop() + .css('display', 'block'); + + $('#unenroll_form input[type="submit"]').prop('disabled', true); + + edx.dashboard.dropdown.toggleCourseActionsDropdownMenu(event); + }); }); $('#unenroll_form').on('ajax:complete', function(event, xhr) { @@ -127,9 +197,10 @@ location.href = urls.signInUser + '?course_id=' + encodeURIComponent($('#unenroll_course_id').val()) + '&enrollment_action=unenroll'; } else { - $('#unenroll_error').html( + $('#unenroll_error').text( xhr.responseText ? xhr.responseText : gettext('An error occurred. Please try again later.') - ).stop().css('display', 'block'); + ).stop() + .css('display', 'block'); } }); @@ -153,7 +224,6 @@ }); $('.action-email-settings').each(function(index) { - $(this).attr('id', 'email-settings-' + index); // a bit of a hack, but gets the unique selector for the modal trigger var trigger = '#' + $(this).attr('id'); accessibleModal( @@ -161,11 +231,11 @@ '#email-settings-modal .close-modal', '#email-settings-modal', '#dashboard-main' - ); + ); + $(this).attr('id', 'email-settings-' + index); }); $('.action-unenroll').each(function(index) { - $(this).attr('id', 'unenroll-' + index); // a bit of a hack, but gets the unique selector for the modal trigger var trigger = '#' + $(this).attr('id'); accessibleModal( @@ -173,7 +243,8 @@ '#unenroll-modal .close-modal', '#unenroll-modal', '#dashboard-main' - ); + ); + $(this).attr('id', 'unenroll-' + index); }); $('#unregister_block_course').click(function(event) { diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index d44bf6c29b..552d815e3b 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -110,13 +110,12 @@ from openedx.core.djangolib.markup import HTML, Text <% credit_status = credit_statuses.get(enrollment.course_id) %> <% show_email_settings = (enrollment.course_id in show_email_settings_for) %> <% course_mode_info = all_course_modes.get(enrollment.course_id) %> - <% show_refund_option = (enrollment.course_id in show_refund_option_for) %> <% is_paid_course = (enrollment.course_id in enrolled_courses_either_paid) %> <% 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) %> <% related_programs = inverted_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, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard' /> + <%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, 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, display_course_modes_on_dashboard=display_course_modes_on_dashboard' /> % endfor diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 97088e942d..ec2874228d 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, related_programs, display_course_modes_on_dashboard" expression_filter="h"/> +<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard" expression_filter="h"/> <%! import urllib @@ -208,6 +208,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_ % endif % endif +
diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index c6c4d7d444..e6d9ee4bd6 100644 --- a/themes/edx.org/lms/templates/dashboard.html +++ b/themes/edx.org/lms/templates/dashboard.html @@ -109,13 +109,12 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers <% credit_status = credit_statuses.get(enrollment.course_id) %> <% show_email_settings = (enrollment.course_id in show_email_settings_for) %> <% course_mode_info = all_course_modes.get(enrollment.course_id) %> - <% show_refund_option = (enrollment.course_id in show_refund_option_for) %> <% is_paid_course = (enrollment.course_id in enrolled_courses_either_paid) %> <% 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) %> <% related_programs = inverted_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, related_programs=related_programs" /> + <%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, 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