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...
This commit is contained in:
@@ -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():
|
||||
|
||||
37
lms/djangoapps/shoppingcart/api.py
Normal file
37
lms/djangoapps/shoppingcart/api.py
Normal file
@@ -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
|
||||
168
lms/djangoapps/shoppingcart/tests/test_microsites.py
Normal file
168
lms/djangoapps/shoppingcart/tests/test_microsites.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,6 +128,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.order-history {
|
||||
span a {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reverify-status-list {
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user