diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py index a486d88f7b..a90f486af6 100644 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -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.") diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index a9e6701622..aaf0f57f85 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -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 diff --git a/lms/djangoapps/instructor/tests/test_ecommerce.py b/lms/djangoapps/instructor/tests/test_ecommerce.py index 7b146cb3a9..52789d17aa 100644 --- a/lms/djangoapps/instructor/tests/test_ecommerce.py +++ b/lms/djangoapps/instructor/tests/test_ecommerce.py @@ -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('Total Amount: $' + str(total_amount) + '' 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('Total Amount: $' + str(total_amount) + '' 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): """ diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index dfb862cf2b..60caded8b5 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -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') ] @@ -875,34 +886,6 @@ def re_validate_invoice(obj_invoice): return JsonResponse({'message': message}) -@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') @@ -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 diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index d7abd9269d..e1d929fbcd 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -19,8 +19,6 @@ urlpatterns = patterns('', # nopep8 'instructor.views.api.get_grading_config', name="get_grading_config"), url(r'^get_students_features(?P/csv)?$', 'instructor.views.api.get_students_features', name="get_students_features"), - url(r'^get_purchase_transaction(?P/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)?$', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 73aa8e839e..614e93af58 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -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 diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index f26ccde17a..d928ddd989 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -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. diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index ad735de250..997311b014 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -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) diff --git a/lms/djangoapps/shoppingcart/exceptions.py b/lms/djangoapps/shoppingcart/exceptions.py index 12358deef6..a5fa8492db 100644 --- a/lms/djangoapps/shoppingcart/exceptions.py +++ b/lms/djangoapps/shoppingcart/exceptions.py @@ -41,6 +41,10 @@ class RegCodeAlreadyExistException(InvalidCartItem): pass +class ItemNotAllowedToRedeemRegCodeException(InvalidCartItem): + pass + + class ItemDoesNotExistAgainstRegCodeException(InvalidCartItem): pass diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index fd87f70081..43737e3def 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -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) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index ea453e77fb..8f48572baa 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -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) diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 9326039ca7..799651910c 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -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") diff --git a/lms/static/coffee/src/instructor_dashboard/e-commerce.coffee b/lms/static/coffee/src/instructor_dashboard/e-commerce.coffee index 026e321b5b..f41b6b0bce 100644 --- a/lms/static/coffee/src/instructor_dashboard/e-commerce.coffee +++ b/lms/static/coffee/src/instructor_dashboard/e-commerce.coffee @@ -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' diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 92965a18aa..79d02ce911 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -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 diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index b4d8eaea4f..0514244897 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -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; diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index 3d8f924cfb..f520d58537 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -53,26 +53,17 @@ %if section_data['access']['finance_admin'] is True: -
-

${_("Transactions")}

-
- %if section_data['total_amount'] is not None: - ${_("Total Amount: ")}${section_data['currency_symbol']}${section_data['total_amount']} - %endif - - ${_("Click to generate a CSV file for all purchase transactions in this course")} - - -
-

${_("Sales")}

+ %if section_data['total_amount'] is not None: + ${_("Total CC Amount: ")}$${section_data['total_amount']} + %endif
${_("Click to generate a CSV file for all sales records in this course")} - +

diff --git a/lms/templates/shoppingcart/billing_details.html b/lms/templates/shoppingcart/billing_details.html index bf35cb7393..3ea256d5af 100644 --- a/lms/templates/shoppingcart/billing_details.html +++ b/lms/templates/shoppingcart/billing_details.html @@ -44,7 +44,7 @@
${form_html}

- ${_('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.')}

diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index 1e4c1b5f93..be14323803 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -1,7 +1,6 @@ <%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">
-
-

${_("Thank you for your Purchase!")}

- % if (len(shoppingcart_items) == 1 and order_type == 'personal') or receipt_has_donation_item: - % for inst in instructions: -

${inst}

- % endfor - % endif -
<% courses_url = reverse('courses') %> - % if order_type == 'personal': + % if receipt_has_donation_item: + ${_("Thank you for your Purchase!")} + % 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') %> ${_("View Dashboard")} - ${_("You have successfully been enrolled for {appended_course_names}. The following receipt has been emailed to" - " {appended_recipient_emails}").format(appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)} + ${_("You have successfully been enrolled for {appended_course_names}. The following receipt has been emailed to" + " {appended_recipient_emails}").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 ${_("Total")}: ${currency_symbol}${"{0:0.2f}".format(order.total_cost)} ${currency.upper()}
- ## 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')}" />
diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index e87027cb5c..88a26c55f4 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -15,15 +15,26 @@ from django.utils.translation import ugettext as _ % if shoppingcart_items: <%block name="billing_details_highlight"> % if order.order_type == 'business': -
  • ${_('Billing Details')}
  • +
  • ${_('Billing Details')}
  • + % else: + % endif - <% discount_applied = False %> + + <% + discount_applied = False + order_type = 'personal' + %> + +
    % for item, course in shoppingcart_items: % if loop.index > 0 :
    %endif + % if item.order.order_type == 'business': + <% order_type = 'business' %> + %endif
    @@ -34,7 +45,7 @@ from django.utils.translation import ugettext as _

    ${_('Registration for:')} ${_('Course Dates:')}

    -

    ${ course.display_name }${course.start_datetime_text()} - ${course.end_datetime_text()}

    +

    ${ course.display_name }

    ${course.start_datetime_text()} - ${course.end_datetime_text()}
    @@ -50,15 +61,15 @@ from django.utils.translation import ugettext as _
    - +
    +
    - +
    -
    +
    @@ -82,24 +93,41 @@ from django.utils.translation import ugettext as _
    %endif - ${_('Total:')} ${currency_symbol}${"{0:0.2f}".format(amount)} ${currency.upper()} + ${_('TOTAL:')} ${currency_symbol}${"{0:0.2f}".format(amount)} ${currency.upper()}
    % if amount == 0: - % elif item.order.order_type == 'business': - -

    - ${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')} -

    - % else: + % elif order_type == 'business': +
    + +

    + ${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')} +

    +
    + + % else: +
    + ${form_html} +

    + ${_('After this purchase is complete,')}
    ${order.user.username} + ${_('will be enrolled in this course.')} +

    +
    + %endif
    @@ -115,6 +143,8 @@ from django.utils.translation import ugettext as _