Commit f733d1d8 by chrisndodge

Merge pull request #6234 from edx/afzaledx/WL-172-order-history

WL-172 Show a Order History list on the Student Dashboard for any PaidCo...
parents a07df1a6 872dbb87
...@@ -48,6 +48,7 @@ from edxmako.shortcuts import render_to_response, render_to_string ...@@ -48,6 +48,7 @@ from edxmako.shortcuts import render_to_response, render_to_string
from mako.exceptions import TopLevelLookupException from mako.exceptions import TopLevelLookupException
from course_modes.models import CourseMode from course_modes.models import CourseMode
from shoppingcart.api import order_history
from student.models import ( from student.models import (
Registration, UserProfile, PendingNameChange, Registration, UserProfile, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user, PendingEmailChange, CourseEnrollment, unique_id_for_user,
...@@ -78,7 +79,6 @@ import external_auth.views ...@@ -78,7 +79,6 @@ import external_auth.views
from bulk_email.models import Optout, CourseAuthorization from bulk_email.models import Optout, CourseAuthorization
import shoppingcart import shoppingcart
from shoppingcart.models import DonationConfiguration
from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.models import UserPreference
from lang_pref import LANGUAGE_KEY from lang_pref import LANGUAGE_KEY
...@@ -104,7 +104,7 @@ from student.helpers import ( ...@@ -104,7 +104,7 @@ from student.helpers import (
check_verify_status_by_course check_verify_status_by_course
) )
from xmodule.error_module import ErrorDescriptor 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 from openedx.core.djangoapps.user_api.api import profile as profile_api
import analytics import analytics
...@@ -641,6 +641,9 @@ def dashboard(request): ...@@ -641,6 +641,9 @@ def dashboard(request):
# otherwise, use the default language # otherwise, use the default language
current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE] 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 = { context = {
'enrollment_message': enrollment_message, 'enrollment_message': enrollment_message,
'course_enrollment_pairs': course_enrollment_pairs, 'course_enrollment_pairs': course_enrollment_pairs,
...@@ -670,6 +673,7 @@ def dashboard(request): ...@@ -670,6 +673,7 @@ def dashboard(request):
'platform_name': settings.PLATFORM_NAME, 'platform_name': settings.PLATFORM_NAME,
'enrolled_courses_either_paid': enrolled_courses_either_paid, 'enrolled_courses_either_paid': enrolled_courses_either_paid,
'provider_states': [], 'provider_states': [],
'order_history_list': order_history_list
} }
if third_party_auth.is_enabled(): if third_party_auth.is_enabled():
......
"""
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
"""
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)
...@@ -1084,10 +1084,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -1084,10 +1084,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
# check for the enrollment codes content # 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) 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 ((template, context), _) = render_mock.call_args # pylint: disable=redefined-outer-name
self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item, context['shoppingcart_items'][0]) 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.purchase_time.strftime("%B %d, %Y"), resp.content)
self.assertIn(self.cart.company_name, resp.content) self.assertIn(self.cart.company_name, resp.content)
self.assertIn(self.cart.company_contact_name, resp.content) self.assertIn(self.cart.company_contact_name, resp.content)
...@@ -1097,6 +1107,25 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -1097,6 +1107,25 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertIn('You have successfully purchased <b>{total_registration_codes} course registration codes' self.assertIn('You have successfully purchased <b>{total_registration_codes} course registration codes'
.format(total_registration_codes=context['total_registration_codes']), resp.content) .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) @patch('shoppingcart.views.render_to_response', render_mock)
def test_show_receipt_success_with_upgrade(self): def test_show_receipt_success_with_upgrade(self):
......
...@@ -750,17 +750,27 @@ def _show_receipt_html(request, order): ...@@ -750,17 +750,27 @@ def _show_receipt_html(request, order):
request.session['attempting_upgrade'] = False request.session['attempting_upgrade'] = False
recipient_list = [] recipient_list = []
registration_codes = None
total_registration_codes = None total_registration_codes = None
reg_code_info_list = []
recipient_list.append(getattr(order.user, 'email')) recipient_list.append(getattr(order.user, 'email'))
if order_type == OrderTypes.BUSINESS: if order_type == OrderTypes.BUSINESS:
registration_codes = CourseRegistrationCode.objects.filter(order=order)
total_registration_codes = registration_codes.count()
if order.company_contact_email: if order.company_contact_email:
recipient_list.append(order.company_contact_email) recipient_list.append(order.company_contact_email)
if order.recipient_email: if order.recipient_email:
recipient_list.append(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) appended_recipient_emails = ", ".join(recipient_list)
context = { context = {
...@@ -775,7 +785,7 @@ def _show_receipt_html(request, order): ...@@ -775,7 +785,7 @@ def _show_receipt_html(request, order):
'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0],
'total_registration_codes': total_registration_codes, '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"), 'order_purchase_date': order.purchase_time.strftime("%B %d, %Y"),
} }
# we want to have the ability to override the default receipt page when # we want to have the ability to override the default receipt page when
......
...@@ -128,6 +128,13 @@ ...@@ -128,6 +128,13 @@
} }
} }
} }
li.order-history {
span a {
font-size: 13px;
line-height: 20px;
}
}
} }
.reverify-status-list { .reverify-status-list {
......
...@@ -852,7 +852,7 @@ ...@@ -852,7 +852,7 @@
text-align: left; text-align: left;
} }
&:last-child{ &:last-child{
text-align: right; text-align: center;
} }
} }
} }
...@@ -865,15 +865,31 @@ ...@@ -865,15 +865,31 @@
padding: 15px 0; padding: 15px 0;
text-align: center; text-align: center;
color: $dark-gray1; color: $dark-gray1;
width: 33.33333%; width: 30%;
&:nth-child(2){width: 20%;}
&:nth-child(3){width: 40%;}
&:first-child{ &:first-child{
text-align: left; text-align: left;
font-size: 18px; font-size: 18px;
text-transform: capitalize; text-transform: capitalize;
} }
&:last-child{ &: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 @@ ...@@ -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;}
} }
}
} }
...@@ -146,6 +146,15 @@ ...@@ -146,6 +146,15 @@
</li> </li>
% endif % 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: % if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
<li class="controls--account"> <li class="controls--account">
<span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span> <span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span>
......
...@@ -52,17 +52,25 @@ from courseware.courses import course_image_url, get_course_about_section, get_c ...@@ -52,17 +52,25 @@ from courseware.courses import course_image_url, get_course_about_section, get_c
<th>${_("Course Name")}</th> <th>${_("Course Name")}</th>
<th>${_("Enrollment Code")}</th> <th>${_("Enrollment Code")}</th>
<th>${_("Enrollment Link")}</th> <th>${_("Enrollment Link")}</th>
<th>${_("Status")}</th>
</thead> </thead>
<tbody> <tbody>
% for registration_code in registration_codes: % for reg_code_info in reg_code_info_list:
<% course = get_course_by_id(registration_code.course_id, depth=0) %>
<tr> <tr>
<td>${_("{course_name}").format(course_name=course.display_name)}</td> <td>${reg_code_info['course_name']}</td>
<td>${registration_code.code}</td> <td>${reg_code_info['code']}</td>
% if reg_code_info['is_redeemed']:
<% redemption_url = reverse('register_code_redemption', args = [registration_code.code] ) %> <td>${reg_code_info['redemption_url']}</td>
<% enrollment_url = '{redemption_url}'.format(redemption_url=redemption_url) %> % else:
<td><a href="${redemption_url}">${enrollment_url}</a></td> <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> </tr>
% endfor % endfor
</tbody> </tbody>
......
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