diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 87a68dd..4c8b08d 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -48,6 +48,7 @@ from edxmako.shortcuts import render_to_response, render_to_string from mako.exceptions import TopLevelLookupException from course_modes.models import CourseMode +from shoppingcart.api import order_history from student.models import ( Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user, @@ -78,7 +79,6 @@ import external_auth.views from bulk_email.models import Optout, CourseAuthorization import shoppingcart -from shoppingcart.models import DonationConfiguration from openedx.core.djangoapps.user_api.models import UserPreference from lang_pref import LANGUAGE_KEY @@ -104,7 +104,7 @@ from student.helpers import ( check_verify_status_by_course ) from xmodule.error_module import ErrorDescriptor -from shoppingcart.models import CourseRegistrationCode +from shoppingcart.models import DonationConfiguration, CourseRegistrationCode from openedx.core.djangoapps.user_api.api import profile as profile_api import analytics @@ -641,6 +641,9 @@ def dashboard(request): # otherwise, use the default language current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE] + # Populate the Order History for the side-bar. + order_history_list = order_history(user, course_org_filter=course_org_filter, org_filter_out_set=org_filter_out_set) + context = { 'enrollment_message': enrollment_message, 'course_enrollment_pairs': course_enrollment_pairs, @@ -670,6 +673,7 @@ def dashboard(request): 'platform_name': settings.PLATFORM_NAME, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], + 'order_history_list': order_history_list } if third_party_auth.is_enabled(): diff --git a/lms/djangoapps/shoppingcart/api.py b/lms/djangoapps/shoppingcart/api.py new file mode 100644 index 0000000..5c7145d --- /dev/null +++ b/lms/djangoapps/shoppingcart/api.py @@ -0,0 +1,37 @@ +""" +API for for getting information about the user's shopping cart. +""" +from django.core.urlresolvers import reverse +from xmodule.modulestore.django import ModuleI18nService +from shoppingcart.models import OrderItem + + +def order_history(user, **kwargs): + """ + Returns the list of previously purchased orders for a user. Only the orders with + PaidCourseRegistration and CourseRegCodeItem are returned. + Params: + course_org_filter: Current Microsite's ORG. + org_filter_out_set: A list of all other Microsites' ORGs. + """ + course_org_filter = kwargs['course_org_filter'] if 'course_org_filter' in kwargs else None + org_filter_out_set = kwargs['org_filter_out_set'] if 'org_filter_out_set' in kwargs else [] + + order_history_list = [] + purchased_order_items = OrderItem.objects.filter(user=user, status='purchased').select_subclasses().order_by('-fulfilled_time') + for order_item in purchased_order_items: + # Avoid repeated entries for the same order id. + if order_item.order.id not in [item['order_id'] for item in order_history_list]: + # If we are in a Microsite, then include the orders having courses attributed (by ORG) to that Microsite. + # Conversely, if we are not in a Microsite, then include the orders having courses + # not attributed (by ORG) to any Microsite. + order_item_course_id = getattr(order_item, 'course_id', None) + if order_item_course_id: + if (course_org_filter and course_org_filter == order_item_course_id.org) or \ + (course_org_filter is None and order_item_course_id.org not in org_filter_out_set): + order_history_list.append({ + 'order_id': order_item.order.id, + 'receipt_url': reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': order_item.order.id}), + 'order_date': ModuleI18nService().strftime(order_item.order.purchase_time, 'SHORT_DATE') + }) + return order_history_list diff --git a/lms/djangoapps/shoppingcart/tests/test_microsites.py b/lms/djangoapps/shoppingcart/tests/test_microsites.py new file mode 100644 index 0000000..72cf8ff --- /dev/null +++ b/lms/djangoapps/shoppingcart/tests/test_microsites.py @@ -0,0 +1,168 @@ +""" +Tests for Microsite Dashboard with Shopping Cart History +""" +import mock + +from django.conf import settings +from django.test.utils import override_settings +from django.core.urlresolvers import reverse + +from mock import patch + +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, mixed_store_config +) +from xmodule.modulestore.tests.factories import CourseFactory +from shoppingcart.models import ( + Order, PaidCourseRegistration, CertificateItem, Donation +) +from student.tests.factories import UserFactory +from course_modes.models import CourseMode + + +# Since we don't need any XML course fixtures, use a modulestore configuration +# that disables the XML modulestore. +MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False) + + +def fake_all_orgs(default=None): # pylint: disable=unused-argument + """ + create a fake list of all microsites + """ + return set(['fakeX', 'fooX']) + + +def fakex_microsite(name, default=None): # pylint: disable=unused-argument + """ + create a fake microsite site name + """ + return 'fakeX' + + +def non_microsite(name, default=None): # pylint: disable=unused-argument + """ + create a fake microsite site name + """ + return None + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) +class TestOrderHistoryOnMicrositeDashboard(ModuleStoreTestCase): + """ + Test for microsite dashboard order history + """ + def setUp(self): + patcher = patch('student.models.tracker') + self.mock_tracker = patcher.start() + self.user = UserFactory.create() + self.user.set_password('password') + self.user.save() + + self.addCleanup(patcher.stop) + + # First Order with our (fakeX) microsite's course. + course1 = CourseFactory.create(org='fakeX', number='999', display_name='fakeX Course') + course1_key = course1.id + course1_mode = CourseMode(course_id=course1_key, + mode_slug="honor", + mode_display_name="honor cert", + min_price=20) + course1_mode.save() + + cart = Order.get_cart_for_user(self.user) + PaidCourseRegistration.add_to_order(cart, course1_key) + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_microsite = cart.id + + # Second Order with another(fooX) microsite's course + course2 = CourseFactory.create(org='fooX', number='888', display_name='fooX Course') + course2_key = course2.id + course2_mode = CourseMode(course_id=course2.id, + mode_slug="honor", + mode_display_name="honor cert", + min_price=20) + course2_mode.save() + + cart = Order.get_cart_for_user(self.user) + PaidCourseRegistration.add_to_order(cart, course2_key) + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_other_microsite = cart.id + + # Third Order with course not attributed to any microsite. + course3 = CourseFactory.create(org='otherorg', number='777', display_name='otherorg Course') + course3_key = course3.id + course3_mode = CourseMode(course_id=course3.id, + mode_slug="honor", + mode_display_name="honor cert", + min_price=20) + course3_mode.save() + + cart = Order.get_cart_for_user(self.user) + PaidCourseRegistration.add_to_order(cart, course3_key) + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_non_microsite = cart.id + + # Fourth Order with course not attributed to any microsite but with a CertificateItem + course4 = CourseFactory.create(org='otherorg', number='888') + course4_key = course4.id + course4_mode = CourseMode(course_id=course4.id, + mode_slug="verified", + mode_display_name="verified cert", + min_price=20) + course4_mode.save() + + cart = Order.get_cart_for_user(self.user) + CertificateItem.add_to_order(cart, course4_key, 20.0, 'verified') + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_cert_non_microsite = cart.id + + # Fifth Order with course not attributed to any microsite but with a Donation + course5 = CourseFactory.create(org='otherorg', number='999') + course5_key = course5.id + + cart = Order.get_cart_for_user(self.user) + Donation.add_to_order(cart, 20.0, course5_key) + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_donation = cart.id + + # also add a donation not associated with a course to make sure the None case works OK + Donation.add_to_order(cart, 10.0, None) + cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + self.orderid_courseless_donation = cart.id + + @mock.patch("microsite_configuration.microsite.get_value", fakex_microsite) + @mock.patch("microsite_configuration.microsite.get_all_orgs", fake_all_orgs) + def test_when_in_microsite_shows_orders_with_microsite_courses_only(self): + self.client.login(username=self.user.username, password="password") + response = self.client.get(reverse("dashboard")) + receipt_url_microsite_course = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_microsite}) + receipt_url_microsite_course2 = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_other_microsite}) + receipt_url_non_microsite = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_non_microsite}) + receipt_url_cert_non_microsite = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_cert_non_microsite}) + receipt_url_donation = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_donation}) + + self.assertIn(receipt_url_microsite_course, response.content) + self.assertNotIn(receipt_url_microsite_course2, response.content) + self.assertNotIn(receipt_url_non_microsite, response.content) + self.assertNotIn(receipt_url_cert_non_microsite, response.content) + self.assertNotIn(receipt_url_donation, response.content) + + @mock.patch("microsite_configuration.microsite.get_value", non_microsite) + @mock.patch("microsite_configuration.microsite.get_all_orgs", fake_all_orgs) + def test_when_not_in_microsite_shows_orders_with_non_microsite_courses_only(self): + self.client.login(username=self.user.username, password="password") + response = self.client.get(reverse("dashboard")) + receipt_url_microsite_course = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_microsite}) + receipt_url_microsite_course2 = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_other_microsite}) + receipt_url_non_microsite = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_non_microsite}) + receipt_url_cert_non_microsite = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_cert_non_microsite}) + receipt_url_donation = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_donation}) + receipt_url_courseless_donation = reverse('shoppingcart.views.show_receipt', kwargs={'ordernum': self.orderid_courseless_donation}) + + self.assertNotIn(receipt_url_microsite_course, response.content) + self.assertNotIn(receipt_url_microsite_course2, response.content) + self.assertIn(receipt_url_non_microsite, response.content) + self.assertIn(receipt_url_cert_non_microsite, response.content) + self.assertIn(receipt_url_donation, response.content) + self.assertIn(receipt_url_courseless_donation, response.content) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 98dc36b..2659440 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1084,10 +1084,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): # check for the enrollment codes content self.assertIn('Please send each professional one of these unique registration codes to enroll into the course.', resp.content) + # fetch the newly generated registration codes + course_registration_codes = CourseRegistrationCode.objects.filter(order=self.cart) + ((template, context), _) = render_mock.call_args # pylint: disable=redefined-outer-name self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(context['order'], self.cart) self.assertIn(reg_item, context['shoppingcart_items'][0]) + # now check for all the registration codes in the receipt + # and all the codes should be unused at this point + self.assertIn(course_registration_codes[0].code, context['reg_code_info_list'][0]['code']) + self.assertIn(course_registration_codes[1].code, context['reg_code_info_list'][1]['code']) + self.assertFalse(context['reg_code_info_list'][0]['is_redeemed']) + self.assertFalse(context['reg_code_info_list'][1]['is_redeemed']) + self.assertIn(self.cart.purchase_time.strftime("%B %d, %Y"), resp.content) self.assertIn(self.cart.company_name, resp.content) self.assertIn(self.cart.company_contact_name, resp.content) @@ -1097,6 +1107,25 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertIn('You have successfully purchased <b>{total_registration_codes} course registration codes' .format(total_registration_codes=context['total_registration_codes']), resp.content) + # now redeem one of registration code from the previous order + redeem_url = reverse('register_code_redemption', args=[context['reg_code_info_list'][0]['code']]) + + #now activate the user by enrolling him/her to the course + response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'}) + self.assertEquals(response.status_code, 200) + self.assertTrue('View Course' in response.content) + + # now view the receipt page again to see if any registration codes + # has been expired or not + resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) + self.assertEqual(resp.status_code, 200) + ((template, context), _) = render_mock.call_args # pylint: disable=redefined-outer-name + self.assertEqual(template, 'shoppingcart/receipt.html') + # now check for all the registration codes in the receipt + # and one of code should be used at this point + self.assertTrue(context['reg_code_info_list'][0]['is_redeemed']) + self.assertFalse(context['reg_code_info_list'][1]['is_redeemed']) + @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success_with_upgrade(self): diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 2286d0b..1f51a09 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -750,17 +750,27 @@ def _show_receipt_html(request, order): request.session['attempting_upgrade'] = False recipient_list = [] - registration_codes = None total_registration_codes = None + reg_code_info_list = [] recipient_list.append(getattr(order.user, 'email')) if order_type == OrderTypes.BUSINESS: - registration_codes = CourseRegistrationCode.objects.filter(order=order) - total_registration_codes = registration_codes.count() if order.company_contact_email: recipient_list.append(order.company_contact_email) if order.recipient_email: recipient_list.append(order.recipient_email) + for __, course in shoppingcart_items: + course_registration_codes = CourseRegistrationCode.objects.filter(order=order, course_id=course.id) + total_registration_codes = course_registration_codes.count() + for course_registration_code in course_registration_codes: + reg_code_info_list.append({ + 'course_name': course.display_name, + 'redemption_url': reverse('register_code_redemption', args=[course_registration_code.code]), + 'code': course_registration_code.code, + 'is_redeemed': RegistrationCodeRedemption.objects.filter( + registration_code=course_registration_code).exists(), + }) + appended_recipient_emails = ", ".join(recipient_list) context = { @@ -775,7 +785,7 @@ def _show_receipt_html(request, order): 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], 'total_registration_codes': total_registration_codes, - 'registration_codes': registration_codes, + 'reg_code_info_list': reg_code_info_list, 'order_purchase_date': order.purchase_time.strftime("%B %d, %Y"), } # we want to have the ability to override the default receipt page when diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 1bc44cf..e3820f3 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -128,6 +128,13 @@ } } } + + li.order-history { + span a { + font-size: 13px; + line-height: 20px; + } + } } .reverify-status-list { diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index 231532c..18848b5 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -852,7 +852,7 @@ text-align: left; } &:last-child{ - text-align: right; + text-align: center; } } } @@ -865,15 +865,31 @@ padding: 15px 0; text-align: center; color: $dark-gray1; - width: 33.33333%; - + width: 30%; + &:nth-child(2){width: 20%;} + &:nth-child(3){width: 40%;} &:first-child{ text-align: left; font-size: 18px; text-transform: capitalize; } &:last-child{ - text-align: right; + text-align: center; + span{ + padding: 2px 10px; + font-size: 13px; + color: #fff; + display: inline-block; + border-radius: 3px; + min-width: 55px; + text-align: center; + &.red{ + background: rgb(231, 92, 92); + } + &.green{ + background: rgb(108, 204, 108); + } + } } } } @@ -953,5 +969,17 @@ } } } + table.course-receipt{ + tr{ + td{ + a{ + &:before{content:" " attr(data-base-url) " ";display: inline-block;} + } + } + } + th:last-child{display: none;} + td:last-child{display: none;} + } } + } diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 75bfc00..07dd71b 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -146,6 +146,15 @@ </li> % endif + % if len(order_history_list): + <li class="order-history"> + <span class="title">${_("Order History")}</span> + % for order_history_item in order_history_list: + <span><a href="${order_history_item['receipt_url']}" target="_blank" class="edit-name">${order_history_item['order_date']}</a></span> + % endfor + </li> + % endif + % if external_auth_map is None or 'shib' not in external_auth_map.external_domain: <li class="controls--account"> <span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span> diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index be14323..e6e2c9d 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -52,17 +52,25 @@ from courseware.courses import course_image_url, get_course_about_section, get_c <th>${_("Course Name")}</th> <th>${_("Enrollment Code")}</th> <th>${_("Enrollment Link")}</th> + <th>${_("Status")}</th> </thead> <tbody> - % for registration_code in registration_codes: - <% course = get_course_by_id(registration_code.course_id, depth=0) %> + % for reg_code_info in reg_code_info_list: <tr> - <td>${_("{course_name}").format(course_name=course.display_name)}</td> - <td>${registration_code.code}</td> - - <% redemption_url = reverse('register_code_redemption', args = [registration_code.code] ) %> - <% enrollment_url = '{redemption_url}'.format(redemption_url=redemption_url) %> - <td><a href="${redemption_url}">${enrollment_url}</a></td> + <td>${reg_code_info['course_name']}</td> + <td>${reg_code_info['code']}</td> + % if reg_code_info['is_redeemed']: + <td>${reg_code_info['redemption_url']}</td> + % else: + <td><a href="${reg_code_info['redemption_url']}" data-base-url="${site_name}">${reg_code_info['redemption_url']}</a></td> + % endif + <td> + % if reg_code_info['is_redeemed']: + <span class="red"></M>${_("Used")}</span> + % else: + <span class="green"></M>${_("Available")}</span> + % endif + </td> </tr> % endfor </tbody>