Improvements and bug fixes for shopping cart: WL-117, WL-114, WL-116, WL-115, WK-122
This commit is contained in:
committed by
Chris Dodge
parent
b99dca987e
commit
cd30164b5f
@@ -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')
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,26 +53,17 @@
|
||||
</div>
|
||||
<!-- end wrap -->
|
||||
%if section_data['access']['finance_admin'] is True:
|
||||
<div class="wrap">
|
||||
<h2>${_("Transactions")}</h2>
|
||||
<div>
|
||||
%if section_data['total_amount'] is not None:
|
||||
<span>${_("Total Amount: ")}<span>${section_data['currency_symbol']}${section_data['total_amount']}</span></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>
|
||||
%if section_data['total_amount'] is not None:
|
||||
<span><strong>${_("Total CC Amount: ")}</strong></span><span>$${section_data['total_amount']}</span>
|
||||
%endif
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
|
||||
@@ -15,15 +15,26 @@ from django.utils.translation import ugettext as _
|
||||
% if shoppingcart_items:
|
||||
<%block name="billing_details_highlight">
|
||||
% if order.order_type == 'business':
|
||||
<li>${_('Billing Details')}</li>
|
||||
<li class="billing">${_('Billing Details')}</li>
|
||||
% else:
|
||||
<li class="billing hidden">${_('Billing Details')}</li>
|
||||
% endif
|
||||
</%block>
|
||||
<% discount_applied = False %>
|
||||
|
||||
<%
|
||||
discount_applied = False
|
||||
order_type = 'personal'
|
||||
%>
|
||||
|
||||
|
||||
<section class="wrapper confirm-enrollment shopping-cart">
|
||||
% for item, course in shoppingcart_items:
|
||||
% if loop.index > 0 :
|
||||
<hr>
|
||||
%endif
|
||||
% if item.order.order_type == 'business':
|
||||
<% order_type = 'business' %>
|
||||
%endif
|
||||
<div class="user-data">
|
||||
<div class="clearfix">
|
||||
<div class="image">
|
||||
@@ -34,7 +45,7 @@ from django.utils.translation import ugettext as _
|
||||
</div>
|
||||
<div class="data-input">
|
||||
<h3>${_('Registration for:')} <span class="pull-right">${_('Course Dates:')}</span></h3>
|
||||
<h1>${ course.display_name }<span class="pull-right">${course.start_datetime_text()} - ${course.end_datetime_text()}</span></h1>
|
||||
<h1>${ course.display_name }</h1><span class="pull-right">${course.start_datetime_text()} - ${course.end_datetime_text()}</span>
|
||||
<hr />
|
||||
<div class="three-col">
|
||||
<div class="col-1">
|
||||
@@ -50,15 +61,15 @@ from django.utils.translation import ugettext as _
|
||||
<div class="numbers-row">
|
||||
<label for="students">${_('Students:')}</label>
|
||||
<div class="counter">
|
||||
<input maxlength="3" max="999" type="text" name="students" value="${item.qty}" id="${item.id}" >
|
||||
<input maxlength="3" title="Input qty and press enter." max="999" type="text" name="students" value="${item.qty}" id="${item.id}" data-unit-cost="${item.unit_cost}" data-qty="${item.qty}">
|
||||
</div>
|
||||
<div class="inc button"><i class="icon-caret-up"><span>+</span></i></div><div class="dec button"><i class="icon-caret-down"></i></div>
|
||||
<a name="updateBtn" class="updateBtn hidden" id="updateBtn-${item.id}" href="#">update</a>
|
||||
<!--<a name="updateBtn" class="updateBtn hidden" id="updateBtn-${item.id}" href="#">update</a>-->
|
||||
<span class="error-text hidden" id="students-${item.id}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3 no-width">
|
||||
<div class="col-3">
|
||||
<a href="#" class="btn-remove" data-item-id="${item.id}"><i class="icon-remove-sign"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,24 +93,41 @@ from django.utils.translation import ugettext as _
|
||||
<input type="submit" value="Reset" class="blue-border" id="submit-reset-redemption">
|
||||
</div>
|
||||
%endif
|
||||
<span class="pull-right">${_('Total:')} <b id="total-amount">${currency_symbol}${"{0:0.2f}".format(amount)} ${currency.upper()}</b></span>
|
||||
<span class="pull-right">${_('TOTAL:')} <b id="total-amount" data-amount="${'{0:0.2f}'.format(amount)}">${currency_symbol}${"{0:0.2f}".format(amount)} ${currency.upper()}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-two">
|
||||
<div class="col-2 relative">
|
||||
% if amount == 0:
|
||||
<input type="submit" value = "Register" id="register" >
|
||||
% elif item.order.order_type == 'business':
|
||||
<input type="submit" value = "Billing Details" id="billing-details"><i class="icon-caret-right"></i>
|
||||
<p>
|
||||
${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')}
|
||||
</p>
|
||||
% else:
|
||||
% elif order_type == 'business':
|
||||
<div name="billing">
|
||||
<input type="submit" value = "Billing Details" name="billing-details"><i class="icon-caret-right"></i>
|
||||
<p>
|
||||
${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')}
|
||||
</p>
|
||||
</div>
|
||||
<div name="payment" class="hidden">
|
||||
${form_html}
|
||||
<p>
|
||||
${_('After this purchase is complete,')}<br/><b>${order.user.username}</b>
|
||||
${_('will be enrolled in this course.')}
|
||||
</p>
|
||||
</div>
|
||||
% else:
|
||||
<div name="payment">
|
||||
${form_html}
|
||||
<p>
|
||||
${_('After this purchase is complete,')}<br/><b>${order.user.username}</b>
|
||||
${_('will be enrolled in this course.')}
|
||||
</p>
|
||||
</div>
|
||||
<div name="billing" class="hidden">
|
||||
<input type="submit" value = "Billing Details" name="billing-details"><i class="icon-caret-right"></i>
|
||||
<p>
|
||||
${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')}
|
||||
</p>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,6 +143,8 @@ from django.utils.translation import ugettext as _
|
||||
</%block>
|
||||
<script>
|
||||
$(function() {
|
||||
var isSpinnerBtnEnabled = true;
|
||||
var prevQty = 0;
|
||||
|
||||
$('a.btn-remove').click(function(event) {
|
||||
event.preventDefault();
|
||||
@@ -180,51 +210,60 @@ from django.utils.translation import ugettext as _
|
||||
})
|
||||
});
|
||||
|
||||
$('#billing-details').click(function(event){
|
||||
$("input[name='billing-details']").click(function(event){
|
||||
event.preventDefault();
|
||||
location.href = "${reverse('shoppingcart.views.billing_details')}";
|
||||
});
|
||||
|
||||
|
||||
$(".button").on("click", function() {
|
||||
var studentField = $(this).parent().find('input');
|
||||
var ItemId = studentField.attr('id');
|
||||
var $button = $(this);
|
||||
var oldValue = $button.parent().find("input").val();
|
||||
var newVal = 1; // initialize with 1.
|
||||
hideErrorMsg('students-'+ItemId);
|
||||
if ($.isNumeric(oldValue)){
|
||||
if ($button.text() == "+") {
|
||||
if(oldValue > 0){
|
||||
newVal = parseFloat(oldValue) + 1;
|
||||
if(newVal > 1000){
|
||||
newVal = 1000;
|
||||
if(isSpinnerBtnEnabled){
|
||||
var isBusinessType = false;
|
||||
var wasBusinessType = false;
|
||||
var studentField = $(this).parent().find("input[type='text']");
|
||||
var unit_cost = parseFloat(studentField.data('unit-cost'));
|
||||
var ItemId = studentField.attr('id');
|
||||
|
||||
var $button = $(this);
|
||||
var oldValue = $("#"+ItemId).data('qty');
|
||||
var newVal = 1; // initialize with 1.
|
||||
oldValue = parseFloat(oldValue);
|
||||
hideErrorMsg('students-'+ItemId);
|
||||
if ($.isNumeric(oldValue)){
|
||||
if ($button.text() == "+") {
|
||||
if(oldValue > 0){
|
||||
newVal = oldValue + 1;
|
||||
if(newVal > 1000){
|
||||
newVal = 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Don't allow decrementing below one
|
||||
if (oldValue > 1) {
|
||||
newVal = oldValue - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Don't allow decrementing below one
|
||||
if (oldValue > 1) {
|
||||
newVal = parseFloat(oldValue) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
$button.parent().find("input").val(newVal);
|
||||
$('#updateBtn-'+ItemId).removeClass('hidden');
|
||||
|
||||
wasBusinessType = getBusinessType();
|
||||
$button.parent().find("input").val(newVal);
|
||||
isBusinessType = getBusinessType();
|
||||
update_user_cart(ItemId, newVal, oldValue, unit_cost, wasBusinessType, isBusinessType);
|
||||
$("#"+ItemId).data('qty', newVal);
|
||||
}
|
||||
});
|
||||
|
||||
$('a[name="updateBtn"]').click(function(event) {
|
||||
var studentField = $(this).parent().find('input');
|
||||
var number_of_students = studentField.val();
|
||||
var ItemId = studentField.attr('id');
|
||||
|
||||
if($.isNumeric(number_of_students) && number_of_students > 0 ){
|
||||
hideErrorMsg('students-'+ItemId);
|
||||
update_user_cart(ItemId, number_of_students);
|
||||
}else{
|
||||
showErrorMsgs('quantity must be greater then 0.', 'students-'+ItemId);
|
||||
}
|
||||
});
|
||||
function getBusinessType(){
|
||||
var isTypeBusiness = false;
|
||||
var elements = $('.numbers-row').find("input[type='text']");
|
||||
elements.each(function(index){
|
||||
if(this.value > 1){
|
||||
isTypeBusiness = true;
|
||||
}
|
||||
});
|
||||
return isTypeBusiness;
|
||||
}
|
||||
|
||||
function showErrorMsgs(msg, msg_area){
|
||||
|
||||
@@ -240,48 +279,133 @@ $('a[name="updateBtn"]').click(function(event) {
|
||||
$( "span.error-text#"+ msg_area +"" ).addClass("hidden");
|
||||
}
|
||||
|
||||
function update_user_cart(ItemId, number_of_students){
|
||||
function getNewTotal(prevQty, newQty, unit_cost, prevTotal){
|
||||
|
||||
var prevQty = parseInt(prevQty);
|
||||
var newQty = parseInt(newQty);
|
||||
var unit_cost = parseFloat(unit_cost);
|
||||
|
||||
return (( newQty - prevQty ) * unit_cost + prevTotal)
|
||||
}
|
||||
|
||||
function update_user_cart(ItemId, newQty, prevQty, unit_cost, wasbusinessType, isbusinessType){
|
||||
|
||||
var post_url = "${reverse('shoppingcart.views.update_user_cart')}";
|
||||
var typeChanged = false;
|
||||
var prevTotal = $('#total-amount').data('amount')
|
||||
var newTotal = getNewTotal(prevQty, newQty, unit_cost, prevTotal);
|
||||
$('#total-amount').html('$'+newTotal.toFixed(2)+' USD');
|
||||
$('#total-amount').data('amount', newTotal);
|
||||
|
||||
if(isbusinessType != wasbusinessType){
|
||||
isSpinnerBtnEnabled = false;
|
||||
typeChanged = true;
|
||||
$('html').css({'cursor':'wait'});
|
||||
$(".button").css({'cursor':'wait'});
|
||||
$('.col-2.relative').find("input[type='submit']").attr('disabled', true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$.post(post_url, {
|
||||
ItemId:ItemId,
|
||||
qty:number_of_students
|
||||
qty:newQty
|
||||
}
|
||||
)
|
||||
.success(function(data) {
|
||||
location.reload(true);
|
||||
var prevTotal = data['total_cost'];
|
||||
$('html').css({'cursor':'default'});
|
||||
$(".button").css({'cursor':'default'});
|
||||
if(typeChanged){
|
||||
var submit_button = $('.col-2.relative').find("input[type='submit']")
|
||||
submit_button.removeAttr('disabled');
|
||||
for (var i = 0; i< data['oldToNewIdMap'].length; i++) {
|
||||
$('#'+data['oldToNewIdMap'][i]['oldId']+'').attr('id',data['oldToNewIdMap'][i]['newId']);
|
||||
$('a.btn-remove[data-item-id]=' +data['oldToNewIdMap'][i]['oldId']+'').data('item-id', data['oldToNewIdMap'][i]['newId']);
|
||||
}
|
||||
if(isbusinessType){
|
||||
$( "div[name='payment']").addClass('hidden');
|
||||
$( "div[name='billing']").removeClass('hidden');
|
||||
$('li.billing').removeClass('hidden');
|
||||
|
||||
}else{
|
||||
$( "div[name='payment']").removeClass('hidden');
|
||||
$( "div[name='billing']").addClass('hidden');
|
||||
$('li.billing').addClass('hidden');
|
||||
}
|
||||
|
||||
// $('#total-amount').html('$'+data['total_cost'].toFixed(2)+' USD');
|
||||
isSpinnerBtnEnabled = true;
|
||||
}
|
||||
|
||||
})
|
||||
.error(function(data,status) {
|
||||
location.reload(true);
|
||||
})
|
||||
}
|
||||
|
||||
$('input[name="students"]').on("click", function() {
|
||||
$('#updateBtn-'+this.id).removeClass('hidden');
|
||||
});
|
||||
$("input[name=students]").keyup(function(event) {
|
||||
var eventEnter = 13;
|
||||
if(event.keyCode == eventEnter){
|
||||
|
||||
updateTextFieldQty(event);
|
||||
}
|
||||
else{
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
function updateTextFieldQty(event){
|
||||
|
||||
if(isSpinnerBtnEnabled){
|
||||
var itemId = event.currentTarget.id;
|
||||
var prevQty = $("#"+itemId).data('qty');
|
||||
var newQty = event.currentTarget.value;
|
||||
var unitCost = event.currentTarget.dataset.unitCost;
|
||||
var isBusinessType = getBusinessType();
|
||||
|
||||
var wasBusinessType = !isBusinessType;
|
||||
isSpinnerBtnEnabled = false;
|
||||
update_user_cart(itemId, newQty, prevQty, unitCost, wasBusinessType, isBusinessType);
|
||||
$("#"+itemId).data('qty', newQty);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$("input[name=students]").focusout(function(event) {
|
||||
updateTextFieldQty(event);
|
||||
});
|
||||
|
||||
// allowing user to enter numeric qty only.
|
||||
$("input[name=students]").keydown(function(event) {
|
||||
var eventDelete = 46;
|
||||
var eventBackSpace = 8;
|
||||
var eventLeftKey = 37;
|
||||
var eventRightKey = 39;
|
||||
var allowedEventCodes = [eventDelete, eventBackSpace, eventLeftKey, eventRightKey ];
|
||||
// Allow only backspace and delete
|
||||
if (allowedEventCodes.indexOf(event.keyCode) > -1) {
|
||||
// let it happen, don't do anything
|
||||
}
|
||||
else {
|
||||
/*
|
||||
Ensure that it is a number.
|
||||
KeyCode range 48 - 57 represents [0-9]
|
||||
KeyCode range 96 - 105 represents [numpad 0 - numpad 9]
|
||||
*/
|
||||
if ((event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >= 96 && event.keyCode <= 105) ) {
|
||||
$('#updateBtn-'+this.id).removeClass('hidden');
|
||||
}else{
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
if(isSpinnerBtnEnabled){
|
||||
var eventDelete = 46;
|
||||
var eventBackSpace = 8;
|
||||
var eventLeftKey = 37;
|
||||
var eventRightKey = 39;
|
||||
var allowedEventCodes = [eventDelete, eventBackSpace, eventLeftKey, eventRightKey ];
|
||||
|
||||
// Allow only backspace and delete
|
||||
if (allowedEventCodes.indexOf(event.keyCode) > -1) {
|
||||
// let it happen, don't do anything
|
||||
}
|
||||
else {
|
||||
/*
|
||||
Ensure that it is a number.
|
||||
KeyCode range 48 - 57 represents [0-9]
|
||||
KeyCode range 96 - 105 represents [numpad 0 - numpad 9]
|
||||
*/
|
||||
if ((event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >= 96 && event.keyCode <= 105) ) {
|
||||
|
||||
}
|
||||
else{
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user