Commit cd30164b by Muhammad Shoaib Committed by Chris Dodge

Improvements and bug fixes for shopping cart: WL-117, WL-114, WL-116, WL-115, WK-122

parent b99dca98
......@@ -79,8 +79,8 @@ class AutoEnrollmentWithCSVTest(UniqueCourseTest):
Given that I am on the Membership tab on the Instructor Dashboard
When I select an image file (a non-csv file) and click the Upload Button
Then I should be shown an Error Notification
And The Notification message should read 'Could not read uploaded file.'
And The Notification message should read 'Make sure that the file you upload is in CSV..'
"""
self.auto_enroll_section.upload_non_csv_file()
self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR))
self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Could not read uploaded file.")
self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Make sure that the file you upload is in CSV format with no extraneous characters or rows.")
......@@ -34,7 +34,7 @@ from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
from django_comment_common.utils import seed_permissions_roles
from microsite_configuration import microsite
from shoppingcart.models import (
RegistrationCodeRedemption, Order,
RegistrationCodeRedemption, Order, CouponRedemption,
PaidCourseRegistration, Coupon, Invoice, CourseRegistrationCode
)
from student.models import (
......@@ -363,7 +363,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
# test the log for email that's send to new created user.
info_log.assert_called_with("user already exists with username '{username}' and email '{email}'".format(username='test_student_1', email='test_student@example.com'))
def test_bad_file_upload_type(self):
def test_file_upload_type_not_csv(self):
"""
Try uploading some non-CSV file and verify that it is rejected
"""
......@@ -372,6 +372,17 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, Log
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertNotEquals(len(data['general_errors']), 0)
self.assertEquals(data['general_errors'][0]['response'], 'Make sure that the file you upload is in CSV format with no extraneous characters or rows.')
def test_bad_file_upload_type(self):
"""
Try uploading some non-CSV file and verify that it is rejected
"""
uploaded_file = SimpleUploadedFile("temp.csv", io.BytesIO(b"some initial binary data: \x00\x01").read())
response = self.client.post(self.url, {'students_list': uploaded_file})
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertNotEquals(len(data['general_errors']), 0)
self.assertEquals(data['general_errors'][0]['response'], 'Could not read uploaded file.')
def test_insufficient_data(self):
......@@ -1712,30 +1723,43 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
response = self.assert_request_status_code(400, url, method="POST", data=data)
self.assertIn("invoice_number must be an integer, {value} provided".format(value=data['invoice_number']), response.content)
def test_get_ecommerce_purchase_features_csv(self):
"""
Test that the response from get_purchase_transaction is in csv format.
"""
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url + '/csv', {})
self.assertEqual(response['Content-Type'], 'text/csv')
def test_get_sale_order_records_features_csv(self):
"""
Test that the response from get_sale_order_records is in csv format.
"""
# add the coupon code for the course
coupon = Coupon(
code='test_code', description='test_description', course_id=self.course.id,
percentage_discount='10', created_by=self.instructor, is_active=True
)
coupon.save()
self.cart.order_type = 'business'
self.cart.save()
self.cart.add_billing_details(company_name='Test Company', company_contact_name='Test',
company_contact_email='test@123', recipient_name='R1',
recipient_email='', customer_reference_number='PO#23')
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
paid_course_reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course.id)
# update the quantity of the cart item paid_course_reg_item
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': paid_course_reg_item.id, 'qty': '4'})
self.assertEqual(resp.status_code, 200)
# apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code})
self.assertEqual(resp.status_code, 200)
self.cart.purchase()
# get the updated item
item = self.cart.orderitem_set.all().select_subclasses()[0]
# get the redeemed coupon information
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order=self.cart)
sale_order_url = reverse('get_sale_order_records', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(sale_order_url)
self.assertEqual(response['Content-Type'], 'text/csv')
self.assertIn('36', response.content.split('\r\n')[1])
self.assertIn(str(item.unit_cost), response.content.split('\r\n')[1],)
self.assertIn(str(item.list_price), response.content.split('\r\n')[1],)
self.assertIn(item.status, response.content.split('\r\n')[1],)
self.assertIn(coupon_redemption[0].coupon.code, response.content.split('\r\n')[1],)
def test_get_sale_records_features_csv(self):
"""
......@@ -1846,64 +1870,6 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
self.assertEqual(res['total_used_codes'], used_codes)
self.assertEqual(res['total_codes'], 5)
def test_get_ecommerce_purchase_features_with_coupon_info(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_purchase_transaction.
"""
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
# using coupon code
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('students', res_json)
for res in res_json['students']:
self.validate_purchased_transaction_response(res, self.cart, self.instructor, self.coupon_code)
def test_get_ecommerce_purchases_features_without_coupon_info(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_purchase_transaction.
"""
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
carts, instructors = ([] for i in range(2))
# purchasing the course by different users
for _ in xrange(3):
test_instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=test_instructor.username, password='test')
cart = Order.get_cart_for_user(test_instructor)
carts.append(cart)
instructors.append(test_instructor)
PaidCourseRegistration.add_to_order(cart, self.course.id)
cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('students', res_json)
for res, i in zip(res_json['students'], xrange(3)):
self.validate_purchased_transaction_response(res, carts[i], instructors[i], 'None')
def validate_purchased_transaction_response(self, res, cart, user, code):
"""
validate purchased transactions attribute values with the response object
"""
item = cart.orderitem_set.all().select_subclasses()[0]
self.assertEqual(res['coupon_code'], code)
self.assertEqual(res['username'], user.username)
self.assertEqual(res['email'], user.email)
self.assertEqual(res['list_price'], item.list_price)
self.assertEqual(res['unit_cost'], item.unit_cost)
self.assertEqual(res['order_id'], cart.id)
self.assertEqual(res['orderitem_id'], item.id)
def test_get_students_features(self):
"""
Test that some minimum of information is formatted
......
......@@ -53,20 +53,21 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content)
# Total amount html should render in e-commerce page, total amount will be 0
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
self.assertTrue('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
self.assertTrue('Download All e-Commerce Purchase' in response.content)
# Order/Invoice sales csv button text should render in e-commerce page
self.assertTrue('Total CC Amount' in response.content)
self.assertTrue('Download All CC Sales' in response.content)
self.assertTrue('Download All Invoice Sales' in response.content)
self.assertTrue('Enter the invoice number to invalidate or re-validate sale' in response.content)
# removing the course finance_admin role of login user
CourseFinanceAdminRole(self.course.id).remove_users(self.instructor)
# total amount should not be visible in e-commerce page if the user is not finance admin
# Order/Invoice sales csv button text should not be visible in e-commerce page if the user is not finance admin
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url)
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
self.assertFalse('Download All e-Commerce Purchase' in response.content)
self.assertFalse('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
self.assertFalse('Download All Order Sales' in response.content)
self.assertFalse('Download All Invoice Sales' in response.content)
self.assertFalse('Enter the invoice number to invalidate or re-validate sale' in response.content)
def test_user_view_course_price(self):
"""
......
......@@ -260,7 +260,15 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man
try:
upload_file = request.FILES.get('students_list')
students = [row for row in csv.reader(upload_file.read().splitlines())]
if upload_file.name.endswith('.csv'):
students = [row for row in csv.reader(upload_file.read().splitlines())]
course = get_course_by_id(course_id)
else:
general_errors.append({
'username': '', 'email': '',
'response': _('Make sure that the file you upload is in CSV format with no extraneous characters or rows.')
})
except Exception: # pylint: disable=broad-except
general_errors.append({
'username': '', 'email': '', 'response': _('Could not read uploaded file.')
......@@ -269,7 +277,6 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man
upload_file.close()
generated_passwords = []
course = get_course_by_id(course_id)
row_num = 0
for student in students:
row_num = row_num + 1
......@@ -804,6 +811,10 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume
('bill_to_postalcode', 'Postal Code'),
('bill_to_country', 'Country'),
('order_type', 'Order Type'),
('status', 'Order Item Status'),
('coupon_code', 'Coupon Code'),
('unit_cost', 'Unit Price'),
('list_price', 'List Price'),
('codes', 'Registration Codes'),
('course_id', 'Course Id')
]
......@@ -878,34 +889,6 @@ def re_validate_invoice(obj_invoice):
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_purchase_transaction(request, course_id, csv=False): # pylint: disable=unused-argument, redefined-outer-name
"""
return the summary of all purchased transactions for a particular course
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
query_features = [
'id', 'username', 'email', 'course_id', 'list_price', 'coupon_code',
'unit_cost', 'purchase_time', 'orderitem_id',
'order_id',
]
student_data = instructor_analytics.basic.purchase_transactions(course_id, query_features)
if not csv:
response_payload = {
'course_id': course_id.to_deprecated_string(),
'students': student_data,
'queried_features': query_features
}
return JsonResponse(response_payload)
else:
header, datarows = instructor_analytics.csvs.format_dictlist(student_data, query_features)
return instructor_analytics.csvs.create_csv_response("e-commerce_purchase_transactions.csv", header, datarows)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_students_features(request, course_id, csv=False): # pylint: disable=redefined-outer-name
"""
Respond with json which contains a summary of all enrolled students profile information.
......@@ -1003,7 +986,11 @@ def save_registration_code(user, course_id, invoice=None, order=None):
return save_registration_code(user, course_id, invoice, order)
course_registration = CourseRegistrationCode(
code=code, course_id=course_id.to_deprecated_string(), created_by=user, invoice=invoice, order=order
code=code,
course_id=course_id.to_deprecated_string(),
created_by=user,
invoice=invoice,
order=order
)
try:
course_registration.save()
......@@ -1100,11 +1087,22 @@ def generate_registration_codes(request, course_id):
UserPreference.set_preference(request.user, INVOICE_KEY, invoice_copy)
sale_invoice = Invoice.objects.create(
total_amount=sale_price, company_name=company_name, company_contact_email=company_contact_email,
company_contact_name=company_contact_name, course_id=course_id, recipient_name=recipient_name,
recipient_email=recipient_email, address_line_1=address_line_1, address_line_2=address_line_2,
address_line_3=address_line_3, city=city, state=state, zip=zip_code, country=country,
internal_reference=internal_reference, customer_reference_number=customer_reference_number
total_amount=sale_price,
company_name=company_name,
company_contact_email=company_contact_email,
company_contact_name=company_contact_name,
course_id=course_id,
recipient_name=recipient_name,
recipient_email=recipient_email,
address_line_1=address_line_1,
address_line_2=address_line_2,
address_line_3=address_line_3,
city=city,
state=state,
zip=zip_code,
country=country,
internal_reference=internal_reference,
customer_reference_number=customer_reference_number
)
registration_codes = []
for _ in range(course_code_number): # pylint: disable=redefined-outer-name
......
......@@ -19,8 +19,6 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^get_students_features(?P<csv>/csv)?$',
'instructor.views.api.get_students_features', name="get_students_features"),
url(r'^get_purchase_transaction(?P<csv>/csv)?$',
'instructor.views.api.get_purchase_transaction', name="get_purchase_transaction"),
url(r'^get_user_invoice_preference$',
'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"),
url(r'^get_sale_records(?P<csv>/csv)?$',
......
......@@ -129,8 +129,8 @@ def _section_e_commerce(course, access):
""" Provide data for the corresponding dashboard section """
course_key = course.id
coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active')
total_amount = None
course_price = None
total_amount = None
course_honor_mode = CourseMode.mode_for_course(course_key, 'honor')
if course_honor_mode and course_honor_mode.min_price > 0:
course_price = course_honor_mode.min_price
......@@ -149,7 +149,6 @@ def _section_e_commerce(course, access):
'sale_validation_url': reverse('sale_validation', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_update_coupon': reverse('update_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_purchase_transaction_url': reverse('get_purchase_transaction', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_sale_records_url': reverse('get_sale_records', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_sale_order_records_url': reverse('get_sale_order_records', kwargs={'course_id': course_key.to_deprecated_string()}),
'instructor_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}),
......@@ -160,8 +159,8 @@ def _section_e_commerce(course, access):
'set_course_mode_url': reverse('set_course_mode_price', kwargs={'course_id': course_key.to_deprecated_string()}),
'download_coupon_codes_url': reverse('get_coupon_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'coupons': coupons,
'total_amount': total_amount,
'course_price': course_price
'course_price': course_price,
'total_amount': total_amount
}
return section_data
......
......@@ -7,6 +7,7 @@ from shoppingcart.models import (
PaidCourseRegistration, CouponRedemption, Invoice, CourseRegCodeItem,
OrderTypes, RegistrationCodeRedemption, CourseRegistrationCode
)
from django.db.models import Q
from django.contrib.auth.models import User
import xmodule.graders as xmgraders
from django.core.exceptions import ObjectDoesNotExist
......@@ -15,7 +16,7 @@ from django.core.exceptions import ObjectDoesNotExist
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
PROFILE_FEATURES = ('name', 'language', 'location', 'year_of_birth', 'gender',
'level_of_education', 'mailing_address', 'goals', 'meta')
ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'order_id')
ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'status')
ORDER_FEATURES = ('purchase_time',)
SALE_FEATURES = ('total_amount', 'company_name', 'company_contact_name', 'company_contact_email', 'recipient_name',
......@@ -42,8 +43,15 @@ def sale_order_record_features(course_id, features):
{'company_name': 'group_C', 'total_codes': '3', total_amount:'total_amount3 in decimal'.}
]
"""
purchased_courses = PaidCourseRegistration.objects.filter(course_id=course_id, status='purchased').order_by('order')
purchased_course_reg_codes = CourseRegCodeItem.objects.filter(course_id=course_id, status='purchased').order_by('order')
purchased_courses = PaidCourseRegistration.objects.filter(
Q(course_id=course_id),
Q(status='purchased') | Q(status='refunded')
).order_by('order')
purchased_course_reg_codes = CourseRegCodeItem.objects.filter(
Q(course_id=course_id),
Q(status='purchased') | Q(status='refunded')
).order_by('order')
def sale_order_info(purchased_course, features):
"""
......@@ -52,6 +60,7 @@ def sale_order_record_features(course_id, features):
sale_order_features = [x for x in SALE_ORDER_FEATURES if x in features]
course_reg_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features]
order_item_features = [x for x in ORDER_ITEM_FEATURES if x in features]
# Extracting order information
sale_order_dict = dict((feature, getattr(purchased_course.order, feature))
......@@ -67,14 +76,25 @@ def sale_order_record_features(course_id, features):
sale_order_dict.update({"total_codes": 'N/A'})
sale_order_dict.update({'total_used_codes': 'N/A'})
# Extracting OrderItem information of unit_cost, list_price and status
order_item_dict = dict((feature, getattr(purchased_course, feature, None))
for feature in order_item_features)
order_item_dict.update({"coupon_code": 'N/A'})
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order_id=purchased_course.order_id)
# if coupon is redeemed against the order, update the information in the order_item_dict
if coupon_redemption.exists():
coupon_codes = [redemption.coupon.code for redemption in coupon_redemption]
order_item_dict.update({'coupon_code': ", ".join(coupon_codes)})
sale_order_dict.update(dict(order_item_dict.items()))
if getattr(purchased_course.order, 'order_type') == OrderTypes.BUSINESS:
registration_codes = CourseRegistrationCode.objects.filter(order=purchased_course.order, course_id=course_id)
sale_order_dict.update({"total_codes": registration_codes.count()})
sale_order_dict.update({'total_used_codes': RegistrationCodeRedemption.objects.filter(registration_code__in=registration_codes).count()})
total_used_codes = RegistrationCodeRedemption.objects.filter(registration_code__in=registration_codes).count()
sale_order_dict.update({'total_used_codes': total_used_codes})
codes = list()
for reg_code in registration_codes:
codes.append(reg_code.code)
codes = [reg_code.code for reg_code in registration_codes]
# Extracting registration code information
obj_course_reg_code = registration_codes.all()[:1].get()
......@@ -88,7 +108,10 @@ def sale_order_record_features(course_id, features):
return sale_order_dict
csv_data = [sale_order_info(purchased_course, features) for purchased_course in purchased_courses]
csv_data.extend([sale_order_info(purchased_course_reg_code, features) for purchased_course_reg_code in purchased_course_reg_codes])
csv_data.extend(
[sale_order_info(purchased_course_reg_code, features)
for purchased_course_reg_code in purchased_course_reg_codes]
)
return csv_data
......@@ -115,14 +138,14 @@ def sale_record_features(course_id, features):
sale_dict = dict((feature, getattr(sale, feature))
for feature in sale_features)
total_used_codes = RegistrationCodeRedemption.objects.filter(registration_code__in=sale.courseregistrationcode_set.all()).count()
total_used_codes = RegistrationCodeRedemption.objects.filter(
registration_code__in=sale.courseregistrationcode_set.all()
).count()
sale_dict.update({"invoice_number": getattr(sale, 'id')})
sale_dict.update({"total_codes": sale.courseregistrationcode_set.all().count()})
sale_dict.update({'total_used_codes': total_used_codes})
codes = list()
for reg_code in sale.courseregistrationcode_set.all():
codes.append(reg_code.code)
codes = [reg_code.code for reg_code in sale.courseregistrationcode_set.all()]
# Extracting registration code information
obj_course_reg_code = sale.courseregistrationcode_set.all()[:1].get()
......@@ -138,59 +161,6 @@ def sale_record_features(course_id, features):
return [sale_records_info(sale, features) for sale in sales]
def purchase_transactions(course_id, features):
"""
Return list of purchased transactions features as dictionaries.
purchase_transactions(course_id, ['username, email','created_by', unit_cost])
would return [
{'username': 'username1', 'email': 'email1', unit_cost:'cost1 in decimal'.}
{'username': 'username2', 'email': 'email2', unit_cost:'cost2 in decimal'.}
{'username': 'username3', 'email': 'email3', unit_cost:'cost3 in decimal'.}
]
"""
purchased_courses = PaidCourseRegistration.objects.filter(course_id=course_id, status='purchased').order_by('user')
def purchase_transactions_info(purchased_course, features):
""" convert purchase transactions to dictionary """
coupon_code_dict = dict()
student_features = [x for x in STUDENT_FEATURES if x in features]
order_features = [x for x in ORDER_FEATURES if x in features]
order_item_features = [x for x in ORDER_ITEM_FEATURES if x in features]
# Extracting user information
student_dict = dict((feature, getattr(purchased_course.user, feature))
for feature in student_features)
# Extracting Order information
order_dict = dict((feature, getattr(purchased_course.order, feature))
for feature in order_features)
# Extracting OrderItem information
order_item_dict = dict((feature, getattr(purchased_course, feature))
for feature in order_item_features)
order_item_dict.update({"orderitem_id": getattr(purchased_course, 'id')})
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order_id=purchased_course.order_id)
if coupon_redemption:
# we format the coupon codes in comma separated way if there are more then one coupon against a order id.
coupon_codes = list()
for redemption in coupon_redemption:
coupon_codes.append(redemption.coupon.code)
coupon_code_dict = {'coupon_code': ", ".join(coupon_codes)}
else:
coupon_code_dict = {'coupon_code': 'None'}
student_dict.update(dict(order_dict.items() + order_item_dict.items() + coupon_code_dict.items()))
student_dict.update({'course_id': course_id.to_deprecated_string()})
return student_dict
return [purchase_transactions_info(purchased_course, features) for purchased_course in purchased_courses]
def enrolled_students_features(course_key, features):
"""
Return list of student features as dictionaries.
......
......@@ -5,10 +5,14 @@ Tests for instructor.basic
from django.test import TestCase
from student.models import CourseEnrollment
from django.core.urlresolvers import reverse
from mock import patch
from student.tests.factories import UserFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order, Invoice, Coupon, CourseRegCodeItem
from shoppingcart.models import (
CourseRegistrationCode, RegistrationCodeRedemption, Order,
Invoice, Coupon, CourseRegCodeItem, CouponRedemption
)
from course_modes.models import CourseMode
from instructor_analytics.basic import (
sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features,
coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
......@@ -89,6 +93,7 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES))
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
""" Test basic course sale records analytics functions. """
def setUp(self):
......@@ -97,6 +102,12 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
"""
super(TestCourseSaleRecordsAnalyticsBasic, self).setUp()
self.course = CourseFactory.create()
self.cost = 40
self.course_mode = CourseMode(
course_id=self.course.id, mode_slug="honor",
mode_display_name="honor cert", min_price=self.cost
)
self.course_mode.save()
self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test')
......@@ -162,19 +173,44 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
('bill_to_postalcode', 'Postal Code'),
('bill_to_country', 'Country'),
('order_type', 'Order Type'),
('status', 'Order Item Status'),
('coupon_code', 'Coupon Code'),
('unit_cost', 'Unit Price'),
('list_price', 'List Price'),
('codes', 'Registration Codes'),
('course_id', 'Course Id')
]
# add the coupon code for the course
coupon = Coupon(
code='test_code',
description='test_description',
course_id=self.course.id,
percentage_discount='10',
created_by=self.instructor,
is_active=True
)
coupon.save()
order = Order.get_cart_for_user(self.instructor)
order.order_type = 'business'
order.save()
order.add_billing_details(company_name='Test Company', company_contact_name='Test',
company_contact_email='test@123', recipient_name='R1',
recipient_email='', customer_reference_number='PO#23')
order.add_billing_details(
company_name='Test Company',
company_contact_name='Test',
company_contact_email='test@123',
recipient_name='R1', recipient_email='',
customer_reference_number='PO#23'
)
CourseRegCodeItem.add_to_order(order, self.course.id, 4)
# apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code})
self.assertEqual(resp.status_code, 200)
order.purchase()
# get the updated item
item = order.orderitem_set.all().select_subclasses()[0]
# get the redeemed coupon information
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order=order)
db_columns = [x[0] for x in query_features]
sale_order_records_list = sale_order_record_features(self.course.id, db_columns)
......@@ -187,6 +223,10 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number)
self.assertEqual(sale_order_record['total_used_codes'], order.registrationcoderedemption_set.all().count())
self.assertEqual(sale_order_record['total_codes'], len(CourseRegistrationCode.objects.filter(order=order)))
self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
self.assertEqual(sale_order_record['list_price'], item.list_price)
self.assertEqual(sale_order_record['status'], item.status)
self.assertEqual(sale_order_record['coupon_code'], coupon_redemption[0].coupon.code)
class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
......@@ -252,8 +292,11 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
]
for i in range(10):
coupon = Coupon(
code='test_code{0}'.format(i), description='test_description', course_id=self.course.id,
percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True
code='test_code{0}'.format(i),
description='test_description',
course_id=self.course.id, percentage_discount='{0}'.format(i),
created_by=self.instructor,
is_active=True
)
coupon.save()
active_coupons = Coupon.objects.filter(course_id=self.course.id, is_active=True)
......
......@@ -41,6 +41,10 @@ class RegCodeAlreadyExistException(InvalidCartItem):
pass
class ItemNotAllowedToRedeemRegCodeException(InvalidCartItem):
pass
class ItemDoesNotExistAgainstRegCodeException(InvalidCartItem):
pass
......
......@@ -41,7 +41,7 @@ from .exceptions import (
InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException,
AlreadyEnrolledInCourseException, CourseDoesNotExistException,
MultipleCouponsNotAllowedException, RegCodeAlreadyExistException,
ItemDoesNotExistAgainstRegCodeException
ItemDoesNotExistAgainstRegCodeException, ItemNotAllowedToRedeemRegCodeException
)
from microsite_configuration import microsite
......@@ -223,6 +223,7 @@ class Order(models.Model):
is_order_type_business = True
items_to_delete = []
old_to_new_id_map = []
if is_order_type_business:
for cart_item in cart_items:
if hasattr(cart_item, 'paidcourseregistration'):
......@@ -232,6 +233,7 @@ class Order(models.Model):
course_reg_code_item.unit_cost = cart_item.unit_cost
course_reg_code_item.save()
items_to_delete.append(cart_item)
old_to_new_id_map.append({"oldId": cart_item.id, "newId": course_reg_code_item.id})
else:
for cart_item in cart_items:
if hasattr(cart_item, 'courseregcodeitem'):
......@@ -241,12 +243,14 @@ class Order(models.Model):
paid_course_registration.unit_cost = cart_item.unit_cost
paid_course_registration.save()
items_to_delete.append(cart_item)
old_to_new_id_map.append({"oldId": cart_item.id, "newId": paid_course_registration.id})
for item in items_to_delete:
item.delete()
self.order_type = OrderTypes.BUSINESS if is_order_type_business else OrderTypes.PERSONAL
self.save()
return old_to_new_id_map
def generate_registration_codes_csv(self, orderitems, site_name):
"""
......@@ -690,6 +694,10 @@ class RegistrationCodeRedemption(models.Model):
for item in cart_items:
if getattr(item, 'course_id'):
if item.course_id == course_reg_code.course_id:
# If the item qty is greater than 1 then the registration code should not be allowed to
# redeem
if item.qty > 1:
raise ItemNotAllowedToRedeemRegCodeException
# If another account tries to use a existing registration code before the student checks out, an
# error message will appear.The reg code is un-reusable.
code_redemption = cls.objects.filter(registration_code=course_reg_code)
......
......@@ -179,12 +179,40 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
resp = self.client.get(billing_url)
self.assertEqual(resp.status_code, 200)
((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/billing_details.html')
# check for the override currency settings in the context
self.assertEqual(context['currency'], 'PKR')
self.assertEqual(context['currency_symbol'], 'Rs')
def test_same_coupon_code_applied_on_multiple_items_in_the_cart(self):
"""
test to check that that the same coupon code applied on multiple
items in the cart.
"""
self.login_user()
# add first course to user cart
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()]))
self.assertEqual(resp.status_code, 200)
# add and apply the coupon code to course in the cart
self.add_coupon(self.course_key, True, self.coupon_code)
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
# now add the same coupon code to the second course(testing_course)
self.add_coupon(self.testing_course.id, True, self.coupon_code)
#now add the second course to cart, the coupon code should be
# applied when adding the second course to the cart
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.testing_course.id.to_deprecated_string()]))
self.assertEqual(resp.status_code, 200)
#now check the user cart and see that the discount has been applied on both the courses
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200)
#first course price is 40$ and the second course price is 20$
# after 10% discount on both the courses the total price will be 18+36 = 54
self.assertIn('54.00', resp.content)
def test_add_course_to_cart_already_in_cart(self):
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
self.login_user()
......@@ -347,6 +375,18 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 404)
self.assertIn("Code '{0}' is not valid for any course in the shopping cart.".format(self.reg_code), resp.content)
def test_cart_item_qty_greater_than_1_against_valid_reg_code(self):
course_key = self.course_key.to_deprecated_string()
self.add_reg_code(course_key)
item = self.add_course_to_user_cart(self.course_key)
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': 4})
self.assertEqual(resp.status_code, 200)
# now update the cart item quantity and then apply the registration code
# it will raise an exception
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code})
self.assertEqual(resp.status_code, 404)
self.assertIn("Cart item quantity should not be greater than 1 when applying activation code", resp.content)
def test_course_discount_for_valid_active_coupon_code(self):
self.add_coupon(self.course_key, True, self.coupon_code)
......
......@@ -29,7 +29,8 @@ from .exceptions import (
ItemAlreadyInCartException, AlreadyEnrolledInCourseException,
CourseDoesNotExistException, ReportTypeDoesNotExistException,
RegCodeAlreadyExistException, ItemDoesNotExistAgainstRegCodeException,
MultipleCouponsNotAllowedException, InvalidCartItem
MultipleCouponsNotAllowedException, InvalidCartItem,
ItemNotAllowedToRedeemRegCodeException
)
from .models import (
Order, OrderTypes,
......@@ -84,13 +85,24 @@ def add_course_to_cart(request, course_id):
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
# All logging from here handled by the model
try:
PaidCourseRegistration.add_to_order(cart, course_key)
paid_course_item = PaidCourseRegistration.add_to_order(cart, course_key)
except CourseDoesNotExistException:
return HttpResponseNotFound(_('The course you requested does not exist.'))
except ItemAlreadyInCartException:
return HttpResponseBadRequest(_('The course {0} is already in your cart.'.format(course_id)))
except AlreadyEnrolledInCourseException:
return HttpResponseBadRequest(_('You are already registered in course {0}.'.format(course_id)))
else:
# in case a coupon redemption code has been applied, new items should also get a discount if applicable.
order = paid_course_item.order
order_items = order.orderitem_set.all().select_subclasses()
redeemed_coupons = CouponRedemption.objects.filter(order=order)
for redeemed_coupon in redeemed_coupons:
if Coupon.objects.filter(code=redeemed_coupon.coupon.code, course_id=course_key, is_active=True).exists():
coupon = Coupon.objects.get(code=redeemed_coupon.coupon.code, course_id=course_key, is_active=True)
CouponRedemption.add_coupon_redemption(coupon, order, order_items)
break # Since only one code can be applied to the cart, we'll just take the first one and then break.
return HttpResponse(_("Course added to cart."))
......@@ -121,9 +133,10 @@ def update_user_cart(request):
item.qty = qty
item.save()
item.order.update_order_type()
old_to_new_id_map = item.order.update_order_type()
total_cost = item.order.total_cost
return JsonResponse({"total_cost": total_cost}, 200)
return JsonResponse({"total_cost": total_cost, "oldToNewIdMap": old_to_new_id_map}, 200)
return HttpResponseBadRequest('Order item not found in request.')
......@@ -367,6 +380,8 @@ def use_registration_code(course_reg, user):
return HttpResponseBadRequest(_("Oops! The code '{0}' you entered is either invalid or expired".format(course_reg.code)))
except ItemDoesNotExistAgainstRegCodeException:
return HttpResponseNotFound(_("Code '{0}' is not valid for any course in the shopping cart.".format(course_reg.code)))
except ItemNotAllowedToRedeemRegCodeException:
return HttpResponseNotFound(_("Cart item quantity should not be greater than 1 when applying activation code"))
return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json")
......
......@@ -9,7 +9,6 @@ class ECommerce
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather elements
@$list_purchase_csv_btn = @$section.find("input[name='list-purchase-transaction-csv']'")
@$list_sale_csv_btn = @$section.find("input[name='list-sale-csv']'")
@$list_order_sale_csv_btn = @$section.find("input[name='list-order-sale-csv']'")
@$download_company_name = @$section.find("input[name='download_company_name']'")
......@@ -26,11 +25,6 @@ class ECommerce
# attach click handlers
# this handler binds to both the download
# and the csv button
@$list_purchase_csv_btn.click (e) =>
url = @$list_purchase_csv_btn.data 'endpoint'
url += '/csv'
location.href = url
@$list_sale_csv_btn.click (e) =>
url = @$list_sale_csv_btn.data 'endpoint'
url += '/csv'
......
......@@ -259,10 +259,14 @@ section.instructor-dashboard-content-2 {
}
}
// type - error
// type - warning
.message-warning {
border-top: 2px solid $warning-color;
background: tint($warning-color,95%);
.message-title {
color: $warning-color;
}
}
// grandfathered
......
......@@ -362,15 +362,23 @@
text-transform: uppercase;
color: $light-gray2;
padding: 0;
line-height: 20px;
}
h1, h1 span{
h1{
font-size: 24px;
color: $dark-gray1;
padding: 0 0 10px 0;
text-transform: capitalize;
span{font-size: 16px;}
width: 700px;
float: left;
}
hr{border-top: 1px solid $dark-gray2;}
span.date{
width: calc(100% - 700px);
float: right;
text-align: right;
}
hr{border-top: 1px solid $dark-gray2;clear: both;}
.three-col{
.col-1{
width: 450px;
......@@ -378,6 +386,8 @@
font-size: 16px;
text-transform: uppercase;
color: $light-gray2;
padding-top: 11px;
font-weight: 400;
.price{
span{
color: $dark-gray1;
......@@ -394,6 +404,7 @@
line-height: 44px;
text-transform: uppercase;
color: $light-gray2;
margin-top: 3px;
.numbers-row{
position: relative;
label{
......@@ -479,12 +490,10 @@
pointer-events: none;
}
}
.no-width {
width: 0px !important;
}
.col-3{
width: 100px;
width: 40px;
float: right;
padding-top: 13px;
a.btn-remove{
float: right;
opacity: 0.8;
......@@ -519,6 +528,7 @@
span{
display: inline-block;
padding: 9px 0px;
margin-right: -20px;
b{
font-weight: 600;
font-size: 24px;
......@@ -665,6 +675,7 @@
&#register{
padding: 18px 30px;
}
&:hover{background: $m-blue-d2;box-shadow: none;}
}
p{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
......@@ -760,6 +771,7 @@
margin: 20px 0;
overflow: hidden;
.message-left{
width: 100%;
float: left;
line-height: 24px;
color: $dark-gray1;
......@@ -772,6 +784,10 @@
margin-left: 10px;
}
}
.mt-7 {
display: block;
margin-top: 7px;
}
}
}
.bordered-bar{
......@@ -803,6 +819,9 @@
margin-bottom: 20px;
padding:20px;
color: $dark-gray1;
h2 {
font-family: $sans-serif;
}
}
hr.border{
border-top: 2px solid $light-gray1;
......
......@@ -54,25 +54,16 @@
<!-- end wrap -->
%if section_data['access']['finance_admin'] is True:
<div class="wrap">
<h2>${_("Transactions")}</h2>
<h2>${_("Sales")}</h2>
<div>
%if section_data['total_amount'] is not None:
<span>${_("Total Amount: ")}<span>${section_data['currency_symbol']}${section_data['total_amount']}</span></span>
<span><strong>${_("Total CC Amount: ")}</strong></span><span>$${section_data['total_amount']}</span>
%endif
<span class="csv_tip">${_("Click to generate a CSV file for all purchase transactions in this course")}
<input class="add blue-button" type="button" name="list-purchase-transaction-csv" value="${_("Download All e-Commerce Purchases")}" data-endpoint="${ section_data['get_purchase_transaction_url'] }" data-csv="true">
</span>
</div>
</div><!-- end wrap -->
<div class="wrap">
<h2>${_("Sales")}</h2>
<div>
<span class="csv_tip">
<div >
${_("Click to generate a CSV file for all sales records in this course")}
<input type="button" class="add blue-button" name="list-sale-csv" value="${_("Download All Invoice Sales")}" data-endpoint="${ section_data['get_sale_records_url'] }" data-csv="true">
<input type="button" class="add blue-button" name="list-order-sale-csv" value="${_("Download All Order Sales")}" data-endpoint="${ section_data['get_sale_order_records_url'] }" data-csv="true">
<input type="button" class="add blue-button" name="list-order-sale-csv" value="${_("Download All CC Sales")}" data-endpoint="${ section_data['get_sale_order_records_url'] }" data-csv="true">
</div>
</span>
<hr>
......
......@@ -44,7 +44,7 @@
<div class="col-2">
${form_html}
<p>
${_('If no additional billing details are populated the payment confirmation will be sent to the user making the purchase')}
${_('If no additional billing details are populated the payment confirmation will be sent to the user making the purchase.')}
</p>
</div>
</div>
......
<%inherit file="shopping_cart_flow.html" />
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%! from microsite_configuration import microsite %>
<%!
from courseware.courses import course_image_url, get_course_about_section, get_course_by_id
%>
......@@ -16,25 +15,22 @@ from courseware.courses import course_image_url, get_course_about_section, get_c
<%block name="custom_content">
<div class="container">
<section class="notification">
<h2>${_("Thank you for your Purchase!")}</h2>
% if (len(shoppingcart_items) == 1 and order_type == 'personal') or receipt_has_donation_item:
% for inst in instructions:
<p>${inst}</p>
% endfor
% endif
</section>
<section class="wrapper confirm-enrollment shopping-cart print">
<div class="gray-bg">
<div class="message-left">
<% courses_url = reverse('courses') %>
% if order_type == 'personal':
% if receipt_has_donation_item:
<b>${_("Thank you for your Purchase!")}</b>
% for inst in instructions:
${inst}
% endfor
% elif order_type == 'personal':
## in case of multiple courses in single self purchase scenario,
## we will show the button View Dashboard
<% dashboard_url = reverse('dashboard') %>
<a href="${dashboard_url}" class="blue pull-right">${_("View Dashboard")} <i class="icon-caret-right"></i></a>
${_("You have successfully been enrolled for <b>{appended_course_names}</b>. The following receipt has been emailed to"
" <strong>{appended_recipient_emails}</strong>").format(appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)}
<span class="mt-7">${_("You have successfully been enrolled for <b>{appended_course_names}</b>. The following receipt has been emailed to"
" <strong>{appended_recipient_emails}</strong></span>").format(appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)}
% elif order_type == 'business':
% if total_registration_codes > 1 :
<% code_plural_form = 'codes' %>
......@@ -351,8 +347,6 @@ from courseware.courses import course_image_url, get_course_about_section, get_c
<span class="pull-right">${_("Total")}: <b> ${currency_symbol}${"{0:0.2f}".format(order.total_cost)} ${currency.upper()}</b></span>
</div>
</div>
## Allow for a microsite to be able to insert additional text at the bottom of the page
<%include file="${microsite.get_template_path('receipt_custom_pane.html')}" />
</section>
</div>
</%block>
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