Commit 31292a68 by Ayub khan Committed by GitHub

Merge pull request #15602 from…

Merge pull request #15602 from edx/adeel/LEARNER_1426_lms_dashboard_on_enrollment_and_refund_orders_on_demand_version

Improve ECOM connection on student dashboard via on demand ajax calls.
parents aaad66d3 e966188b
......@@ -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."""
......
......@@ -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):
......
......@@ -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"),
)
......
......@@ -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 = {
......
......@@ -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"""
......
......@@ -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' <span id="unenroll_course_name">' + course_name + u'</span>' + \
u' (<span id="unenroll_course_number">' + course_number + u'</span>)?'
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' <span id="unenroll_cert_name">' + cert_long_name + u'</span>' + \
u' track of <span id="unenroll_course_name">' + course_name + u'</span>' + \
u' (<span id="unenroll_course_number">' + course_number + u'</span>)?'
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):
"""
......
......@@ -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: "<span id='unenroll_course_number'>" + course_number + '</span>',
course_name: "<span id='unenroll_course_name'>" + course_name + '</span>',
cert_name_long: "<span id='unenroll_cert_name'>" + cert_name_long + '</span>'
}, 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: ['<span id="unenroll_course_number">', courseNumber, '</span>'].join(''),
courseName: ['<span id="unenroll_course_name">', courseName, '</span>'].join(''),
certNameLong: ['<span id="unenroll_cert_name">', certNameLong, '</span>'].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) {
......
......@@ -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
</ul>
......
......@@ -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
</ul>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment