Merge pull request #5235 from edx/cdodge/shopping-cart-rewrite
Cdodge/shopping cart rewrite
This commit is contained in:
@@ -446,13 +446,17 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
|
||||
"""Checking either registration is blocked or not ."""
|
||||
blocked = False
|
||||
for redeemed_registration in redeemed_registration_codes:
|
||||
if not getattr(redeemed_registration.invoice, 'is_valid'):
|
||||
blocked = True
|
||||
# disabling email notifications for unpaid registration courses
|
||||
Optout.objects.get_or_create(user=request.user, course_id=course_key)
|
||||
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(request.user.username, request.user.email, course_key))
|
||||
track.views.server_track(request, "change-email1-settings", {"receive_emails": "no", "course": course_key.to_deprecated_string()}, page='dashboard')
|
||||
break
|
||||
# registration codes may be generated via Bulk Purchase Scenario
|
||||
# we have to check only for the invoice generated registration codes
|
||||
# that their invoice is valid or not
|
||||
if redeemed_registration.invoice:
|
||||
if not getattr(redeemed_registration.invoice, 'is_valid'):
|
||||
blocked = True
|
||||
# disabling email notifications for unpaid registration courses
|
||||
Optout.objects.get_or_create(user=request.user, course_id=course_key)
|
||||
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(request.user.username, request.user.email, course_key))
|
||||
track.views.server_track(request, "change-email1-settings", {"receive_emails": "no", "course": course_key.to_deprecated_string()}, page='dashboard')
|
||||
break
|
||||
|
||||
return blocked
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from course_modes.models import CourseMode
|
||||
|
||||
from open_ended_grading import open_ended_notifications
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from student.views import single_course_reverification_info
|
||||
from student.views import single_course_reverification_info, is_course_blocked
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from xblock.fragment import Fragment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -277,12 +277,22 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
|
||||
# Redirecting to dashboard if the course is blocked due to un payment.
|
||||
redeemed_registration_codes = CourseRegistrationCode.objects.filter(course_id=course_key, registrationcoderedemption__redeemed_by=request.user)
|
||||
for redeemed_registration in redeemed_registration_codes:
|
||||
if not getattr(redeemed_registration.invoice, 'is_valid'):
|
||||
log.warning(u'User %s cannot access the course %s because payment has not yet been received', user, course_key.to_deprecated_string())
|
||||
return redirect(reverse('dashboard'))
|
||||
redeemed_registration_codes = CourseRegistrationCode.objects.filter(
|
||||
course_id=course_key,
|
||||
registrationcoderedemption__redeemed_by=request.user
|
||||
)
|
||||
|
||||
# Redirect to dashboard if the course is blocked due to non-payment.
|
||||
if is_course_blocked(request, redeemed_registration_codes, course_key):
|
||||
# registration codes may be generated via Bulk Purchase Scenario
|
||||
# we have to check only for the invoice generated registration codes
|
||||
# that their invoice is valid or not
|
||||
log.warning(
|
||||
u'User %s cannot access the course %s because payment has not yet been received',
|
||||
user,
|
||||
course_key.to_deprecated_string()
|
||||
)
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
request.user = user # keep just one instance of User
|
||||
with modulestore().bulk_operations(course_key):
|
||||
@@ -703,7 +713,8 @@ def course_about(request, course_id):
|
||||
settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
|
||||
if request.user.is_authenticated():
|
||||
cart = shoppingcart.models.Order.get_cart_for_user(request.user)
|
||||
in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key)
|
||||
in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order(cart, course_key) or \
|
||||
shoppingcart.models.CourseRegCodeItem.contained_in_order(cart, course_key)
|
||||
|
||||
reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
|
||||
reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string())
|
||||
|
||||
@@ -1448,6 +1448,21 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
self.cart.purchase()
|
||||
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')
|
||||
|
||||
def test_get_sale_records_features_csv(self):
|
||||
"""
|
||||
Test that the response from get_sale_records is in csv format.
|
||||
|
||||
@@ -588,7 +588,47 @@ def get_sale_records(request, course_id, csv=False): # pylint: disable=W0613, W
|
||||
return JsonResponse(response_payload)
|
||||
else:
|
||||
header, datarows = instructor_analytics.csvs.format_dictlist(sale_data, query_features)
|
||||
return instructor_analytics.csvs.create_csv_response("e-commerce_sale_records.csv", header, datarows)
|
||||
return instructor_analytics.csvs.create_csv_response("e-commerce_sale_invoice_records.csv", header, datarows)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def get_sale_order_records(request, course_id): # pylint: disable=W0613, W0621
|
||||
"""
|
||||
return the summary of all sales records for a particular course
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
query_features = [
|
||||
('id', 'Order Id'),
|
||||
('company_name', 'Company Name'),
|
||||
('company_contact_name', 'Company Contact Name'),
|
||||
('company_contact_email', 'Company Contact Email'),
|
||||
('total_amount', 'Total Amount'),
|
||||
('total_codes', 'Total Codes'),
|
||||
('total_used_codes', 'Total Used Codes'),
|
||||
('logged_in_username', 'Login Username'),
|
||||
('logged_in_email', 'Login User Email'),
|
||||
('purchase_time', 'Date of Sale'),
|
||||
('customer_reference_number', 'Customer Reference Number'),
|
||||
('recipient_name', 'Recipient Name'),
|
||||
('recipient_email', 'Recipient Email'),
|
||||
('bill_to_street1', 'Street 1'),
|
||||
('bill_to_street2', 'Street 2'),
|
||||
('bill_to_city', 'City'),
|
||||
('bill_to_state', 'State'),
|
||||
('bill_to_postalcode', 'Postal Code'),
|
||||
('bill_to_country', 'Country'),
|
||||
('order_type', 'Order Type'),
|
||||
('codes', 'Registration Codes'),
|
||||
('course_id', 'Course Id')
|
||||
]
|
||||
|
||||
db_columns = [x[0] for x in query_features]
|
||||
csv_columns = [x[1] for x in query_features]
|
||||
sale_data = instructor_analytics.basic.sale_order_record_features(course_id, db_columns)
|
||||
header, datarows = instructor_analytics.csvs.format_dictlist(sale_data, db_columns) # pylint: disable=W0612
|
||||
return instructor_analytics.csvs.create_csv_response("e-commerce_sale_order_records.csv", csv_columns, datarows)
|
||||
|
||||
|
||||
@require_level('staff')
|
||||
@@ -766,7 +806,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=W0613
|
||||
return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows)
|
||||
|
||||
|
||||
def save_registration_codes(request, course_id, generated_codes_list, invoice):
|
||||
def save_registration_code(user, course_id, invoice=None, order=None):
|
||||
"""
|
||||
recursive function that generate a new code every time and saves in the Course Registration Table
|
||||
if validation check passes
|
||||
@@ -776,16 +816,16 @@ def save_registration_codes(request, course_id, generated_codes_list, invoice):
|
||||
# check if the generated code is in the Coupon Table
|
||||
matching_coupons = Coupon.objects.filter(code=code, is_active=True)
|
||||
if matching_coupons:
|
||||
return save_registration_codes(request, course_id, generated_codes_list, invoice)
|
||||
return save_registration_code(user, course_id, invoice, order)
|
||||
|
||||
course_registration = CourseRegistrationCode(
|
||||
code=code, course_id=course_id.to_deprecated_string(), created_by=request.user, invoice=invoice
|
||||
code=code, course_id=course_id.to_deprecated_string(), created_by=user, invoice=invoice, order=order
|
||||
)
|
||||
try:
|
||||
course_registration.save()
|
||||
generated_codes_list.append(course_registration)
|
||||
return course_registration
|
||||
except IntegrityError:
|
||||
return save_registration_codes(request, course_id, generated_codes_list, invoice)
|
||||
return save_registration_code(user, course_id, invoice, order)
|
||||
|
||||
|
||||
def registration_codes_csv(file_name, codes_list, csv_type=None):
|
||||
@@ -851,7 +891,6 @@ def generate_registration_codes(request, course_id):
|
||||
Respond with csv which contains a summary of all Generated Codes.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_registration_codes = []
|
||||
invoice_copy = False
|
||||
|
||||
# covert the course registration code number into integer
|
||||
@@ -888,8 +927,10 @@ def generate_registration_codes(request, course_id):
|
||||
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=W0621
|
||||
save_registration_codes(request, course_id, course_registration_codes, sale_invoice)
|
||||
generated_registration_code = save_registration_code(request.user, course_id, sale_invoice, order=None)
|
||||
registration_codes.append(generated_registration_code)
|
||||
|
||||
site_name = microsite.get_value('SITE_NAME', 'localhost')
|
||||
course = get_course_by_id(course_id, depth=None)
|
||||
@@ -916,7 +957,7 @@ def generate_registration_codes(request, course_id):
|
||||
'discount': discount,
|
||||
'sale_price': sale_price,
|
||||
'quantity': quantity,
|
||||
'registration_codes': course_registration_codes,
|
||||
'registration_codes': registration_codes,
|
||||
'course_url': course_url,
|
||||
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
|
||||
'dashboard_url': dashboard_url,
|
||||
@@ -934,7 +975,7 @@ def generate_registration_codes(request, course_id):
|
||||
#send_mail(subject, message, from_address, recipient_list, fail_silently=False)
|
||||
csv_file = StringIO.StringIO()
|
||||
csv_writer = csv.writer(csv_file)
|
||||
for registration_code in course_registration_codes:
|
||||
for registration_code in registration_codes:
|
||||
csv_writer.writerow([registration_code.code])
|
||||
|
||||
# send a unique email for each recipient, don't put all email addresses in a single email
|
||||
@@ -948,7 +989,7 @@ def generate_registration_codes(request, course_id):
|
||||
email.attach(u'Invoice.txt', invoice_attachment, 'text/plain')
|
||||
email.send()
|
||||
|
||||
return registration_codes_csv("Registration_Codes.csv", course_registration_codes)
|
||||
return registration_codes_csv("Registration_Codes.csv", registration_codes)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -23,6 +23,8 @@ urlpatterns = patterns('', # nopep8
|
||||
'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"),
|
||||
url(r'^get_sale_records(?P<csv>/csv)?$',
|
||||
'instructor.views.api.get_sale_records', name="get_sale_records"),
|
||||
url(r'^get_sale_order_records$',
|
||||
'instructor.views.api.get_sale_order_records', name="get_sale_order_records"),
|
||||
url(r'^sale_validation_url$',
|
||||
'instructor.views.api.sale_validation', name="sale_validation"),
|
||||
url(r'^get_anon_ids$',
|
||||
|
||||
@@ -62,27 +62,36 @@ def add_coupon(request, course_id): # pylint: disable=W0613
|
||||
# check if the coupon code is in the CourseRegistrationCode Table
|
||||
course_registration_code = CourseRegistrationCode.objects.filter(code=code)
|
||||
if course_registration_code:
|
||||
return HttpResponseNotFound(_(
|
||||
"The code ({code}) that you have tried to define is already in use as a registration code").format(code=code)
|
||||
)
|
||||
return JsonResponse(
|
||||
{'message': _("The code ({code}) that you have tried to define is already in use as a registration code").format(code=code)},
|
||||
status=400) # status code 400: Bad Request
|
||||
|
||||
description = request.POST.get('description')
|
||||
course_id = request.POST.get('course_id')
|
||||
try:
|
||||
discount = int(request.POST.get('discount'))
|
||||
except ValueError:
|
||||
return HttpResponseNotFound(_("Please Enter the Integer Value for Coupon Discount"))
|
||||
return JsonResponse({
|
||||
'message': _("Please Enter the Integer Value for Coupon Discount")
|
||||
}, status=400) # status code 400: Bad Request
|
||||
|
||||
if discount > 100 or discount < 0:
|
||||
return HttpResponseNotFound(_("Please Enter the Coupon Discount Value Less than or Equal to 100"))
|
||||
return JsonResponse({
|
||||
'message': _("Please Enter the Coupon Discount Value Less than or Equal to 100")
|
||||
}, status=400) # status code 400: Bad Request
|
||||
coupon = Coupon(
|
||||
code=code, description=description, course_id=course_id,
|
||||
percentage_discount=discount, created_by_id=request.user.id
|
||||
)
|
||||
coupon.save()
|
||||
return HttpResponse(_("coupon with the coupon code ({code}) added successfully").format(code=code))
|
||||
return JsonResponse(
|
||||
{'message': _("coupon with the coupon code ({code}) added successfully").format(code=code)}
|
||||
)
|
||||
|
||||
if coupon:
|
||||
return HttpResponseNotFound(_("coupon with the coupon code ({code}) already exists for this course").format(code=code))
|
||||
return JsonResponse(
|
||||
{'message': _("coupon with the coupon code ({code}) already exists for this course").format(code=code)},
|
||||
status=400) # status code 400: Bad Request
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -93,17 +102,21 @@ def update_coupon(request, course_id): # pylint: disable=W0613
|
||||
"""
|
||||
coupon_id = request.POST.get('coupon_id', None)
|
||||
if not coupon_id:
|
||||
return HttpResponseNotFound(_("coupon id not found"))
|
||||
return JsonResponse({'message': _("coupon id not found")}, status=400) # status code 400: Bad Request
|
||||
|
||||
try:
|
||||
coupon = Coupon.objects.get(pk=coupon_id)
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) DoesNotExist").format(coupon_id=coupon_id))
|
||||
return JsonResponse(
|
||||
{'message': _("coupon with the coupon id ({coupon_id}) DoesNotExist").format(coupon_id=coupon_id)},
|
||||
status=400) # status code 400: Bad Request
|
||||
|
||||
description = request.POST.get('description')
|
||||
coupon.description = description
|
||||
coupon.save()
|
||||
return HttpResponse(_("coupon with the coupon id ({coupon_id}) updated Successfully").format(coupon_id=coupon_id))
|
||||
return JsonResponse(
|
||||
{'message': _("coupon with the coupon id ({coupon_id}) updated Successfully").format(coupon_id=coupon_id)}
|
||||
)
|
||||
|
||||
|
||||
@require_POST
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.utils.html import escape
|
||||
from django.http import Http404, HttpResponse, HttpResponseNotFound
|
||||
from django.conf import settings
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
from xmodule_modifiers import wrap_xblock
|
||||
@@ -158,6 +159,7 @@ def _section_e_commerce(course, access):
|
||||
'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()}),
|
||||
'get_registration_code_csv_url': reverse('get_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'generate_registration_code_csv_url': reverse('generate_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
@@ -183,15 +185,19 @@ def set_course_mode_price(request, course_id):
|
||||
try:
|
||||
course_price = int(request.POST['course_price'])
|
||||
except ValueError:
|
||||
return HttpResponseNotFound(_("Please Enter the numeric value for the course price"))
|
||||
return JsonResponse(
|
||||
{'message': _("Please Enter the numeric value for the course price")},
|
||||
status=400) # status code 400: Bad Request
|
||||
|
||||
currency = request.POST['currency']
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
course_honor_mode = CourseMode.objects.filter(mode_slug='honor', course_id=course_key)
|
||||
if not course_honor_mode:
|
||||
return HttpResponseNotFound(
|
||||
_("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor')
|
||||
)
|
||||
return JsonResponse(
|
||||
{'message': _("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor')},
|
||||
status=400) # status code 400: Bad Request
|
||||
|
||||
CourseModesArchive.objects.create(
|
||||
course_id=course_id, mode_slug='honor', mode_display_name='Honor Code Certificate',
|
||||
min_price=getattr(course_honor_mode[0], 'min_price'), currency=getattr(course_honor_mode[0], 'currency'),
|
||||
@@ -201,7 +207,7 @@ def set_course_mode_price(request, course_id):
|
||||
min_price=course_price,
|
||||
currency=currency
|
||||
)
|
||||
return HttpResponse(_("CourseMode price updated successfully"))
|
||||
return JsonResponse({'message': _("CourseMode price updated successfully")})
|
||||
|
||||
|
||||
def _section_course_info(course, access):
|
||||
|
||||
@@ -3,7 +3,10 @@ Student and course analytics.
|
||||
|
||||
Serve miscellaneous course and student data
|
||||
"""
|
||||
from shoppingcart.models import PaidCourseRegistration, CouponRedemption, Invoice, RegistrationCodeRedemption
|
||||
from shoppingcart.models import (
|
||||
PaidCourseRegistration, CouponRedemption, Invoice, CourseRegCodeItem,
|
||||
OrderTypes, RegistrationCodeRedemption, CourseRegistrationCode
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule.graders as xmgraders
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -18,11 +21,77 @@ ORDER_FEATURES = ('purchase_time',)
|
||||
SALE_FEATURES = ('total_amount', 'company_name', 'company_contact_name', 'company_contact_email', 'recipient_name',
|
||||
'recipient_email', 'customer_reference_number', 'internal_reference')
|
||||
|
||||
SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_contact_email', 'purchase_time',
|
||||
'customer_reference_number', 'recipient_name', 'recipient_email', 'bill_to_street1',
|
||||
'bill_to_street2', 'bill_to_city', 'bill_to_state', 'bill_to_postalcode',
|
||||
'bill_to_country', 'order_type',)
|
||||
|
||||
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
|
||||
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at')
|
||||
COUPON_FEATURES = ('course_id', 'percentage_discount', 'description')
|
||||
|
||||
|
||||
def sale_order_record_features(course_id, features):
|
||||
"""
|
||||
Return list of sale orders features as dictionaries.
|
||||
|
||||
sales_records(course_id, ['company_name, total_codes', total_amount])
|
||||
would return [
|
||||
{'company_name': 'group_A', 'total_codes': '1', total_amount:'total_amount1 in decimal'.}
|
||||
{'company_name': 'group_B', 'total_codes': '2', total_amount:'total_amount2 in decimal'.}
|
||||
{'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')
|
||||
|
||||
def sale_order_info(purchased_course, features):
|
||||
"""
|
||||
convert purchase transactions to dictionary
|
||||
"""
|
||||
|
||||
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]
|
||||
|
||||
# Extracting order information
|
||||
sale_order_dict = dict((feature, getattr(purchased_course.order, feature))
|
||||
for feature in sale_order_features)
|
||||
|
||||
quantity = int(getattr(purchased_course, 'qty'))
|
||||
unit_cost = float(getattr(purchased_course, 'unit_cost'))
|
||||
sale_order_dict.update({"total_amount": quantity * unit_cost})
|
||||
|
||||
sale_order_dict.update({"logged_in_username": purchased_course.order.user.username})
|
||||
sale_order_dict.update({"logged_in_email": purchased_course.order.user.email})
|
||||
|
||||
sale_order_dict.update({"total_codes": 'N/A'})
|
||||
sale_order_dict.update({'total_used_codes': 'N/A'})
|
||||
|
||||
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()})
|
||||
|
||||
codes = list()
|
||||
for reg_code in registration_codes:
|
||||
codes.append(reg_code.code)
|
||||
|
||||
# Extracting registration code information
|
||||
obj_course_reg_code = registration_codes.all()[:1].get()
|
||||
course_reg_dict = dict((feature, getattr(obj_course_reg_code, feature))
|
||||
for feature in course_reg_features)
|
||||
|
||||
course_reg_dict['course_id'] = course_id.to_deprecated_string()
|
||||
course_reg_dict.update({'codes': ", ".join(codes)})
|
||||
sale_order_dict.update(dict(course_reg_dict.items()))
|
||||
|
||||
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])
|
||||
return csv_data
|
||||
|
||||
|
||||
def sale_record_features(course_id, features):
|
||||
"""
|
||||
Return list of sales features as dictionaries.
|
||||
@@ -73,7 +142,7 @@ def purchase_transactions(course_id, features):
|
||||
"""
|
||||
Return list of purchased transactions features as dictionaries.
|
||||
|
||||
purchase_transactions(course_id, ['username, email', unit_cost])
|
||||
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'.}
|
||||
|
||||
@@ -7,11 +7,11 @@ from student.models import CourseEnrollment
|
||||
from django.core.urlresolvers import reverse
|
||||
from student.tests.factories import UserFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order, Invoice, Coupon
|
||||
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order, Invoice, Coupon, CourseRegCodeItem
|
||||
|
||||
from instructor_analytics.basic import (
|
||||
sale_record_features, enrolled_students_features, course_registration_features, coupon_codes_features,
|
||||
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features,
|
||||
coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
)
|
||||
from course_groups.tests.helpers import CohortFactory
|
||||
from course_groups.models import CourseUserGroup
|
||||
@@ -137,6 +137,57 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
|
||||
self.assertEqual(sale_record['total_used_codes'], 0)
|
||||
self.assertEqual(sale_record['total_codes'], 5)
|
||||
|
||||
def test_sale_order_features(self):
|
||||
"""
|
||||
Test Order Sales Report CSV
|
||||
"""
|
||||
query_features = [
|
||||
('id', 'Order Id'),
|
||||
('company_name', 'Company Name'),
|
||||
('company_contact_name', 'Company Contact Name'),
|
||||
('company_contact_email', 'Company Contact Email'),
|
||||
('total_amount', 'Total Amount'),
|
||||
('total_codes', 'Total Codes'),
|
||||
('total_used_codes', 'Total Used Codes'),
|
||||
('logged_in_username', 'Login Username'),
|
||||
('logged_in_email', 'Login User Email'),
|
||||
('purchase_time', 'Date of Sale'),
|
||||
('customer_reference_number', 'Customer Reference Number'),
|
||||
('recipient_name', 'Recipient Name'),
|
||||
('recipient_email', 'Recipient Email'),
|
||||
('bill_to_street1', 'Street 1'),
|
||||
('bill_to_street2', 'Street 2'),
|
||||
('bill_to_city', 'City'),
|
||||
('bill_to_state', 'State'),
|
||||
('bill_to_postalcode', 'Postal Code'),
|
||||
('bill_to_country', 'Country'),
|
||||
('order_type', 'Order Type'),
|
||||
('codes', 'Registration Codes'),
|
||||
('course_id', 'Course Id')
|
||||
]
|
||||
|
||||
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')
|
||||
CourseRegCodeItem.add_to_order(order, self.course.id, 4)
|
||||
order.purchase()
|
||||
|
||||
db_columns = [x[0] for x in query_features]
|
||||
sale_order_records_list = sale_order_record_features(self.course.id, db_columns)
|
||||
|
||||
for sale_order_record in sale_order_records_list:
|
||||
self.assertEqual(sale_order_record['recipient_email'], order.recipient_email)
|
||||
self.assertEqual(sale_order_record['recipient_name'], order.recipient_name)
|
||||
self.assertEqual(sale_order_record['company_name'], order.company_name)
|
||||
self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name)
|
||||
self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email)
|
||||
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)))
|
||||
|
||||
|
||||
class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
|
||||
""" Test basic course registration codes analytics functions. """
|
||||
|
||||
@@ -21,6 +21,6 @@ def user_has_cart_context_processor(request):
|
||||
settings.FEATURES.get('ENABLE_SHOPPING_CART') and # settings enable shopping cart and
|
||||
shoppingcart.models.Order.user_cart_has_items(
|
||||
request.user,
|
||||
shoppingcart.models.PaidCourseRegistration
|
||||
[shoppingcart.models.PaidCourseRegistration, shoppingcart.models.CourseRegCodeItem]
|
||||
) # user's cart has PaidCourseRegistrations
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'CourseRegCodeItem'
|
||||
db.create_table('shoppingcart_courseregcodeitem', (
|
||||
('orderitem_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['shoppingcart.OrderItem'], unique=True, primary_key=True)),
|
||||
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=128, db_index=True)),
|
||||
('mode', self.gf('django.db.models.fields.SlugField')(default='honor', max_length=50)),
|
||||
))
|
||||
db.send_create_signal('shoppingcart', ['CourseRegCodeItem'])
|
||||
|
||||
# Adding model 'CourseRegCodeItemAnnotation'
|
||||
db.create_table('shoppingcart_courseregcodeitemannotation', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=128, db_index=True)),
|
||||
('annotation', self.gf('django.db.models.fields.TextField')(null=True)),
|
||||
))
|
||||
db.send_create_signal('shoppingcart', ['CourseRegCodeItemAnnotation'])
|
||||
|
||||
# Adding field 'Order.company_name'
|
||||
db.add_column('shoppingcart_order', 'company_name',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.company_contact_name'
|
||||
db.add_column('shoppingcart_order', 'company_contact_name',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.company_contact_email'
|
||||
db.add_column('shoppingcart_order', 'company_contact_email',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.recipient_name'
|
||||
db.add_column('shoppingcart_order', 'recipient_name',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.recipient_email'
|
||||
db.add_column('shoppingcart_order', 'recipient_email',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.customer_reference_number'
|
||||
db.add_column('shoppingcart_order', 'customer_reference_number',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=63, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'Order.order_type'
|
||||
db.add_column('shoppingcart_order', 'order_type',
|
||||
self.gf('django.db.models.fields.CharField')(default='personal', max_length=32),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseRegCodeItem'
|
||||
db.delete_table('shoppingcart_courseregcodeitem')
|
||||
|
||||
# Deleting model 'CourseRegCodeItemAnnotation'
|
||||
db.delete_table('shoppingcart_courseregcodeitemannotation')
|
||||
|
||||
# Deleting field 'Order.company_name'
|
||||
db.delete_column('shoppingcart_order', 'company_name')
|
||||
|
||||
# Deleting field 'Order.company_contact_name'
|
||||
db.delete_column('shoppingcart_order', 'company_contact_name')
|
||||
|
||||
# Deleting field 'Order.company_contact_email'
|
||||
db.delete_column('shoppingcart_order', 'company_contact_email')
|
||||
|
||||
# Deleting field 'Order.recipient_name'
|
||||
db.delete_column('shoppingcart_order', 'recipient_name')
|
||||
|
||||
# Deleting field 'Order.recipient_email'
|
||||
db.delete_column('shoppingcart_order', 'recipient_email')
|
||||
|
||||
# Deleting field 'Order.customer_reference_number'
|
||||
db.delete_column('shoppingcart_order', 'customer_reference_number')
|
||||
|
||||
# Deleting field 'Order.order_type'
|
||||
db.delete_column('shoppingcart_order', 'order_type')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'shoppingcart.certificateitem': {
|
||||
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.coupon': {
|
||||
'Meta': {'object_name': 'Coupon'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'shoppingcart.couponredemption': {
|
||||
'Meta': {'object_name': 'CouponRedemption'},
|
||||
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.courseregcodeitem': {
|
||||
'Meta': {'object_name': 'CourseRegCodeItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.courseregcodeitemannotation': {
|
||||
'Meta': {'object_name': 'CourseRegCodeItemAnnotation'},
|
||||
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.courseregistrationcode': {
|
||||
'Meta': {'object_name': 'CourseRegistrationCode'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"})
|
||||
},
|
||||
'shoppingcart.donation': {
|
||||
'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.donationconfiguration': {
|
||||
'Meta': {'object_name': 'DonationConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.invoice': {
|
||||
'Meta': {'object_name': 'Invoice'},
|
||||
'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'total_amount': ('django.db.models.fields.FloatField', [], {}),
|
||||
'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'})
|
||||
},
|
||||
'shoppingcart.order': {
|
||||
'Meta': {'object_name': 'Order'},
|
||||
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
|
||||
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order_type': ('django.db.models.fields.CharField', [], {'default': "'personal'", 'max_length': '32'}),
|
||||
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.orderitem': {
|
||||
'Meta': {'object_name': 'OrderItem'},
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
|
||||
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
|
||||
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.paidcourseregistration': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.paidcourseregistrationannotation': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
|
||||
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.registrationcoderedemption': {
|
||||
'Meta': {'object_name': 'RegistrationCodeRedemption'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}),
|
||||
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)', 'null': 'True'}),
|
||||
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['shoppingcart']
|
||||
@@ -6,7 +6,9 @@ from decimal import Decimal
|
||||
import pytz
|
||||
import logging
|
||||
import smtplib
|
||||
|
||||
import StringIO
|
||||
import csv
|
||||
from courseware.courses import get_course_by_id
|
||||
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
@@ -19,6 +21,7 @@ from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.core.urlresolvers import reverse
|
||||
from model_utils.managers import InheritanceManager
|
||||
from django.core.mail.message import EmailMessage
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -62,6 +65,19 @@ ORDER_STATUSES = (
|
||||
OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103
|
||||
|
||||
|
||||
class OrderTypes(object):
|
||||
"""
|
||||
This class specify purchase OrderTypes.
|
||||
"""
|
||||
PERSONAL = 'personal'
|
||||
BUSINESS = 'business'
|
||||
|
||||
ORDER_TYPES = (
|
||||
(PERSONAL, 'personal'),
|
||||
(BUSINESS, 'business'),
|
||||
)
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
"""
|
||||
This is the model for an order. Before purchase, an Order and its related OrderItems are used
|
||||
@@ -88,6 +104,15 @@ class Order(models.Model):
|
||||
# a JSON dump of the CC processor response, for completeness
|
||||
processor_reply_dump = models.TextField(blank=True)
|
||||
|
||||
# bulk purchase registration code workflow billing details
|
||||
company_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
company_contact_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
company_contact_email = models.CharField(max_length=255, null=True, blank=True)
|
||||
recipient_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
recipient_email = models.CharField(max_length=255, null=True, blank=True)
|
||||
customer_reference_number = models.CharField(max_length=63, null=True, blank=True)
|
||||
order_type = models.CharField(max_length=32, default='personal', choices=OrderTypes.ORDER_TYPES)
|
||||
|
||||
@classmethod
|
||||
def get_cart_for_user(cls, user):
|
||||
"""
|
||||
@@ -102,7 +127,7 @@ class Order(models.Model):
|
||||
return cart_order
|
||||
|
||||
@classmethod
|
||||
def user_cart_has_items(cls, user, item_type=None):
|
||||
def user_cart_has_items(cls, user, item_types=None):
|
||||
"""
|
||||
Returns true if the user (anonymous user ok) has
|
||||
a cart with items in it. (Which means it should be displayed.
|
||||
@@ -112,7 +137,17 @@ class Order(models.Model):
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
cart = cls.get_cart_for_user(user)
|
||||
return cart.has_items(item_type)
|
||||
|
||||
if not item_types:
|
||||
# check to see if the cart has at least some item in it
|
||||
return cart.has_items()
|
||||
else:
|
||||
# if the caller is explicitly asking to check for particular types
|
||||
for item_type in item_types:
|
||||
if cart.has_items(item_type):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
@@ -130,17 +165,27 @@ class Order(models.Model):
|
||||
if not item_type:
|
||||
return self.orderitem_set.exists() # pylint: disable=E1101
|
||||
else:
|
||||
items = self.orderitem_set.all().select_subclasses()
|
||||
items = self.orderitem_set.all().select_subclasses() # pylint: disable=E1101
|
||||
for item in items:
|
||||
if isinstance(item, item_type):
|
||||
return True
|
||||
return False
|
||||
|
||||
def reset_cart_items_prices(self):
|
||||
"""
|
||||
Reset the items price state in the user cart
|
||||
"""
|
||||
for item in self.orderitem_set.all(): # pylint: disable=E1101
|
||||
if item.list_price:
|
||||
item.unit_cost = item.list_price
|
||||
item.list_price = None
|
||||
item.save()
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear out all the items in the cart
|
||||
"""
|
||||
self.orderitem_set.all().delete()
|
||||
self.orderitem_set.all().delete() # pylint: disable=E1101
|
||||
|
||||
@transaction.commit_on_success
|
||||
def start_purchase(self):
|
||||
@@ -158,6 +203,122 @@ class Order(models.Model):
|
||||
for item in OrderItem.objects.filter(order=self).select_subclasses():
|
||||
item.start_purchase()
|
||||
|
||||
def update_order_type(self):
|
||||
"""
|
||||
updating order type. This method wil inspect the quantity associated with the OrderItem.
|
||||
In the application, it is implied that when qty > 1, then the user is to purchase
|
||||
'RegistrationCodes' which are randomly generated strings that users can distribute to
|
||||
others in order for them to enroll in paywalled courses.
|
||||
|
||||
The UI/UX may change in the future to make the switching between PaidCourseRegistration
|
||||
and CourseRegCodeItems a more explicit UI gesture from the purchaser
|
||||
"""
|
||||
cart_items = self.orderitem_set.all() # pylint: disable=E1101
|
||||
is_order_type_business = False
|
||||
for cart_item in cart_items:
|
||||
if cart_item.qty > 1:
|
||||
is_order_type_business = True
|
||||
|
||||
items_to_delete = []
|
||||
if is_order_type_business:
|
||||
for cart_item in cart_items:
|
||||
if hasattr(cart_item, 'paidcourseregistration'):
|
||||
CourseRegCodeItem.add_to_order(self, cart_item.paidcourseregistration.course_id, cart_item.qty)
|
||||
items_to_delete.append(cart_item)
|
||||
else:
|
||||
for cart_item in cart_items:
|
||||
if hasattr(cart_item, 'courseregcodeitem'):
|
||||
PaidCourseRegistration.add_to_order(self, cart_item.courseregcodeitem.course_id)
|
||||
items_to_delete.append(cart_item)
|
||||
# CourseRegCodeItem.add_to_order
|
||||
|
||||
for item in items_to_delete:
|
||||
item.delete()
|
||||
|
||||
self.order_type = OrderTypes.BUSINESS if is_order_type_business else OrderTypes.PERSONAL
|
||||
self.save()
|
||||
|
||||
def generate_registration_codes_csv(self, orderitems, site_name):
|
||||
"""
|
||||
this function generates the csv file
|
||||
"""
|
||||
course_info = []
|
||||
csv_file = StringIO.StringIO()
|
||||
csv_writer = csv.writer(csv_file)
|
||||
csv_writer.writerow(['Course Name', 'Registration Code', 'URL'])
|
||||
for item in orderitems:
|
||||
course_id = item.course_id
|
||||
course = get_course_by_id(getattr(item, 'course_id'), depth=0)
|
||||
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id, order=self)
|
||||
course_info.append((course.display_name, ' (' + course.start_date_text + '-' + course.end_date_text + ')'))
|
||||
for registration_code in registration_codes:
|
||||
redemption_url = reverse('register_code_redemption', args=[registration_code.code])
|
||||
url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url)
|
||||
csv_writer.writerow([course.display_name, registration_code.code, url])
|
||||
|
||||
return csv_file, course_info
|
||||
|
||||
def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, site_name, courses_info):
|
||||
"""
|
||||
send confirmation e-mail
|
||||
"""
|
||||
recipient_list = [(self.user.username, getattr(self.user, 'email'), 'user')] # pylint: disable=E1101
|
||||
if self.company_contact_email:
|
||||
recipient_list.append((self.company_contact_name, self.company_contact_email, 'company_contact'))
|
||||
joined_course_names = ""
|
||||
if self.recipient_email:
|
||||
recipient_list.append((self.recipient_name, self.recipient_email, 'email_recipient'))
|
||||
courses_names_with_dates = [course_info[0] + course_info[1] for course_info in courses_info]
|
||||
joined_course_names = " " + ", ".join(courses_names_with_dates)
|
||||
|
||||
if not is_order_type_business:
|
||||
subject = _("Order Payment Confirmation")
|
||||
else:
|
||||
subject = _('Confirmation and Registration Codes for the following courses: {course_name_list}').format(
|
||||
course_name_list=joined_course_names
|
||||
)
|
||||
|
||||
dashboard_url = '{base_url}{dashboard}'.format(
|
||||
base_url=site_name,
|
||||
dashboard=reverse('dashboard')
|
||||
)
|
||||
try:
|
||||
from_address = microsite.get_value(
|
||||
'email_from_address',
|
||||
settings.PAYMENT_SUPPORT_EMAIL
|
||||
)
|
||||
# send a unique email for each recipient, don't put all email addresses in a single email
|
||||
for recipient in recipient_list:
|
||||
message = render_to_string(
|
||||
'emails/business_order_confirmation_email.txt' if is_order_type_business else 'emails/order_confirmation_email.txt',
|
||||
{
|
||||
'order': self,
|
||||
'recipient_name': recipient[0],
|
||||
'recipient_type': recipient[2],
|
||||
'site_name': site_name,
|
||||
'order_items': orderitems,
|
||||
'course_names': ", ".join([course_info[0] for course_info in courses_info]),
|
||||
'dashboard_url': dashboard_url,
|
||||
'order_placed_by': '{username} ({email})'.format(username=self.user.username, email=getattr(self.user, 'email')), # pylint: disable=E1101
|
||||
'has_billing_info': settings.FEATURES['STORE_BILLING_INFO'],
|
||||
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
|
||||
'payment_support_email': microsite.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL),
|
||||
'payment_email_signature': microsite.get_value('payment_email_signature'),
|
||||
}
|
||||
)
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=message,
|
||||
from_email=from_address,
|
||||
to=[recipient[1]]
|
||||
)
|
||||
email.content_subtype = "html"
|
||||
if csv_file:
|
||||
email.attach(u'RegistrationCodesRedemptionUrls.csv', csv_file.getvalue(), 'text/csv')
|
||||
email.send()
|
||||
except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually
|
||||
log.error('Failed sending confirmation e-mail for order %d', self.id) # pylint: disable=E1101
|
||||
|
||||
def purchase(self, first='', last='', street1='', street2='', city='', state='', postalcode='',
|
||||
country='', ccnum='', cardtype='', processor_reply_dump=''):
|
||||
"""
|
||||
@@ -200,29 +361,48 @@ class Order(models.Model):
|
||||
# this should return all of the objects with the correct types of the
|
||||
# subclasses
|
||||
orderitems = OrderItem.objects.filter(order=self).select_subclasses()
|
||||
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
|
||||
|
||||
if self.order_type == OrderTypes.BUSINESS:
|
||||
self.update_order_type()
|
||||
|
||||
for item in orderitems:
|
||||
item.purchase_item()
|
||||
|
||||
# send confirmation e-mail
|
||||
subject = _("Order Payment Confirmation")
|
||||
message = render_to_string(
|
||||
'emails/order_confirmation_email.txt',
|
||||
{
|
||||
'order': self,
|
||||
'order_items': orderitems,
|
||||
'has_billing_info': settings.FEATURES['STORE_BILLING_INFO']
|
||||
}
|
||||
)
|
||||
try:
|
||||
from_address = microsite.get_value(
|
||||
'email_from_address',
|
||||
settings.DEFAULT_FROM_EMAIL
|
||||
)
|
||||
csv_file = None
|
||||
courses_info = []
|
||||
if self.order_type == OrderTypes.BUSINESS:
|
||||
#
|
||||
# Generate the CSV file that contains all of the RegistrationCodes that have already been
|
||||
# generated when the purchase has transacted
|
||||
#
|
||||
csv_file, courses_info = self.generate_registration_codes_csv(orderitems, site_name)
|
||||
|
||||
send_mail(subject, message,
|
||||
from_address, [self.user.email]) # pylint: disable=E1101
|
||||
except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually
|
||||
log.error('Failed sending confirmation e-mail for order %d', self.id) # pylint: disable=E1101
|
||||
self.send_confirmation_emails(orderitems, self.order_type == OrderTypes.BUSINESS, csv_file, site_name, courses_info)
|
||||
|
||||
def add_billing_details(self, company_name='', company_contact_name='', company_contact_email='', recipient_name='',
|
||||
recipient_email='', customer_reference_number=''):
|
||||
"""
|
||||
This function is called after the user selects a purchase type of "Business" and
|
||||
is asked to enter the optional billing details. The billing details are updated
|
||||
for that order.
|
||||
|
||||
company_name - Name of purchasing organization
|
||||
company_contact_name - Name of the key contact at the company the sale was made to
|
||||
company_contact_email - Email of the key contact at the company the sale was made to
|
||||
recipient_name - Name of the company should the invoice be sent to
|
||||
recipient_email - Email of the company should the invoice be sent to
|
||||
customer_reference_number - purchase order number of the organization associated with this Order
|
||||
"""
|
||||
|
||||
self.company_name = company_name
|
||||
self.company_contact_name = company_contact_name
|
||||
self.company_contact_email = company_contact_email
|
||||
self.recipient_name = recipient_name
|
||||
self.recipient_email = recipient_email
|
||||
self.customer_reference_number = customer_reference_number
|
||||
|
||||
self.save()
|
||||
|
||||
def generate_receipt_instructions(self):
|
||||
"""
|
||||
@@ -420,6 +600,16 @@ class RegistrationCodeRedemption(models.Model):
|
||||
redeemed_by = models.ForeignKey(User, db_index=True)
|
||||
redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True)
|
||||
|
||||
@classmethod
|
||||
def delete_registration_redemption(cls, user, cart):
|
||||
"""
|
||||
This method delete registration redemption
|
||||
"""
|
||||
reg_code_redemption = cls.objects.filter(redeemed_by=user, order=cart)
|
||||
if reg_code_redemption:
|
||||
reg_code_redemption.delete()
|
||||
log.info('Registration code redemption entry removed for user {0} for order {1}'.format(user, cart.id))
|
||||
|
||||
@classmethod
|
||||
def add_reg_code_redemption(cls, course_reg_code, order):
|
||||
"""
|
||||
@@ -502,6 +692,16 @@ class CouponRedemption(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
coupon = models.ForeignKey(Coupon, db_index=True)
|
||||
|
||||
@classmethod
|
||||
def delete_coupon_redemption(cls, user, cart):
|
||||
"""
|
||||
This method delete coupon redemption
|
||||
"""
|
||||
coupon_redemption = cls.objects.filter(user=user, order=cart)
|
||||
if coupon_redemption:
|
||||
coupon_redemption.delete()
|
||||
log.info('Coupon redemption entry removed for user {0} for order {1}'.format(user, cart.id))
|
||||
|
||||
@classmethod
|
||||
def get_discount_price(cls, percentage_discount, value):
|
||||
"""
|
||||
@@ -665,6 +865,142 @@ class PaidCourseRegistration(OrderItem):
|
||||
return u""
|
||||
|
||||
|
||||
class CourseRegCodeItem(OrderItem):
|
||||
"""
|
||||
This is an inventory item for paying for
|
||||
generating course registration codes
|
||||
"""
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
|
||||
|
||||
@classmethod
|
||||
def contained_in_order(cls, order, course_id):
|
||||
"""
|
||||
Is the course defined by course_id contained in the order?
|
||||
"""
|
||||
return course_id in [
|
||||
item.course_id
|
||||
for item in order.orderitem_set.all().select_subclasses("courseregcodeitem")
|
||||
if isinstance(item, cls)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_total_amount_of_purchased_item(cls, course_key):
|
||||
"""
|
||||
This will return the total amount of money that a purchased course generated
|
||||
"""
|
||||
total_cost = 0
|
||||
result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=E1101
|
||||
|
||||
if result['total'] is not None:
|
||||
total_cost = result['total']
|
||||
|
||||
return total_cost
|
||||
|
||||
@classmethod
|
||||
@transaction.commit_on_success
|
||||
def add_to_order(cls, order, course_id, qty, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None): # pylint: disable=W0221
|
||||
"""
|
||||
A standardized way to create these objects, with sensible defaults filled in.
|
||||
Will update the cost if called on an order that already carries the course.
|
||||
|
||||
Returns the order item
|
||||
"""
|
||||
# First a bunch of sanity checks
|
||||
course = modulestore().get_course(course_id) # actually fetch the course to make sure it exists, use this to
|
||||
# throw errors if it doesn't
|
||||
if not course:
|
||||
log.error("User {} tried to add non-existent course {} to cart id {}"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise CourseDoesNotExistException
|
||||
|
||||
if cls.contained_in_order(order, course_id):
|
||||
log.warning("User {} tried to add PaidCourseRegistration for course {}, already in cart id {}"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise ItemAlreadyInCartException
|
||||
|
||||
if CourseEnrollment.is_enrolled(user=order.user, course_key=course_id):
|
||||
log.warning("User {} trying to add course {} to cart id {}, already registered"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise AlreadyEnrolledInCourseException
|
||||
|
||||
### Validations done, now proceed
|
||||
### handle default arguments for mode_slug, cost, currency
|
||||
course_mode = CourseMode.mode_for_course(course_id, mode_slug)
|
||||
if not course_mode:
|
||||
# user could have specified a mode that's not set, in that case return the DEFAULT_MODE
|
||||
course_mode = CourseMode.DEFAULT_MODE
|
||||
if not cost:
|
||||
cost = course_mode.min_price
|
||||
if not currency:
|
||||
currency = course_mode.currency
|
||||
|
||||
super(CourseRegCodeItem, cls).add_to_order(order, course_id, cost, currency=currency)
|
||||
|
||||
item, created = cls.objects.get_or_create(order=order, user=order.user, course_id=course_id) # pylint: disable=W0612
|
||||
item.status = order.status
|
||||
item.mode = course_mode.slug
|
||||
item.unit_cost = cost
|
||||
item.qty = qty
|
||||
item.line_desc = _(u'Enrollment codes for Course: {course_name}').format(
|
||||
course_name=course.display_name_with_default)
|
||||
item.currency = currency
|
||||
order.currency = currency
|
||||
item.report_comments = item.csv_report_comments
|
||||
order.save()
|
||||
item.save()
|
||||
log.info("User {} added course registration {} to cart: order {}"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
return item
|
||||
|
||||
def purchased_callback(self):
|
||||
"""
|
||||
The purchase is completed, this OrderItem type will generate Registration Codes that will
|
||||
be redeemed by users
|
||||
"""
|
||||
if not modulestore().has_course(self.course_id):
|
||||
raise PurchasedCallbackException(
|
||||
"The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id))
|
||||
total_registration_codes = int(self.qty)
|
||||
|
||||
# we need to import here because of a circular dependency
|
||||
# we should ultimately refactor code to have save_registration_code in this models.py
|
||||
# file, but there's also a shared dependency on a random string generator which
|
||||
# is in another PR (for another feature)
|
||||
from instructor.views.api import save_registration_code
|
||||
for i in range(total_registration_codes): # pylint: disable=W0612
|
||||
save_registration_code(self.user, self.course_id, invoice=None, order=self.order)
|
||||
|
||||
log.info("Enrolled {0} in paid course {1}, paid ${2}"
|
||||
.format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=E1101
|
||||
|
||||
@property
|
||||
def csv_report_comments(self):
|
||||
"""
|
||||
Tries to fetch an annotation associated with the course_id from the database. If not found, returns u"".
|
||||
Otherwise returns the annotation
|
||||
"""
|
||||
try:
|
||||
return CourseRegCodeItemAnnotation.objects.get(course_id=self.course_id).annotation
|
||||
except CourseRegCodeItemAnnotation.DoesNotExist:
|
||||
return u""
|
||||
|
||||
|
||||
class CourseRegCodeItemAnnotation(models.Model):
|
||||
"""
|
||||
A model that maps course_id to an additional annotation. This is specifically needed because when Stanford
|
||||
generates report for the paid courses, each report item must contain the payment account associated with a course.
|
||||
And unfortunately we didn't have the concept of a "SKU" or stock item where we could keep this association,
|
||||
so this is to retrofit it.
|
||||
"""
|
||||
course_id = CourseKeyField(unique=True, max_length=128, db_index=True)
|
||||
annotation = models.TextField(null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
# pylint: disable=no-member
|
||||
return u"{} : {}".format(self.course_id.to_deprecated_string(), self.annotation)
|
||||
|
||||
|
||||
class PaidCourseRegistrationAnnotation(models.Model):
|
||||
"""
|
||||
A model that maps course_id to an additional annotation. This is specifically needed because when Stanford
|
||||
@@ -1011,3 +1347,9 @@ class Donation(OrderItem):
|
||||
# The donation is for the organization as a whole, not a specific course
|
||||
else:
|
||||
return _(u"Donation for {platform_name}").format(platform_name=settings.PLATFORM_NAME)
|
||||
|
||||
@property
|
||||
def single_item_receipt_context(self):
|
||||
return {
|
||||
'receipt_has_donation_item': True,
|
||||
}
|
||||
|
||||
@@ -19,15 +19,17 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from shoppingcart.models import (
|
||||
Order, OrderItem, CertificateItem,
|
||||
InvalidCartItem, PaidCourseRegistration,
|
||||
InvalidCartItem, CourseRegistrationCode, PaidCourseRegistration, CourseRegCodeItem,
|
||||
Donation, OrderItemSubclassPK
|
||||
)
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from shoppingcart.exceptions import PurchasedCallbackException, CourseDoesNotExistException
|
||||
from shoppingcart.exceptions import (PurchasedCallbackException, CourseDoesNotExistException,
|
||||
ItemAlreadyInCartException, AlreadyEnrolledInCourseException)
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
@@ -63,21 +65,21 @@ class OrderTest(ModuleStoreTestCase):
|
||||
item = OrderItem(order=cart, user=self.user)
|
||||
item.save()
|
||||
self.assertTrue(Order.user_cart_has_items(self.user))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, CertificateItem))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, PaidCourseRegistration))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, [CertificateItem]))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, [PaidCourseRegistration]))
|
||||
|
||||
def test_user_cart_has_paid_course_registration_items(self):
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
item = PaidCourseRegistration(order=cart, user=self.user)
|
||||
item.save()
|
||||
self.assertTrue(Order.user_cart_has_items(self.user, PaidCourseRegistration))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, CertificateItem))
|
||||
self.assertTrue(Order.user_cart_has_items(self.user, [PaidCourseRegistration]))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, [CertificateItem]))
|
||||
|
||||
def test_user_cart_has_certificate_items(self):
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
|
||||
self.assertTrue(Order.user_cart_has_items(self.user, CertificateItem))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, PaidCourseRegistration))
|
||||
self.assertTrue(Order.user_cart_has_items(self.user, [CertificateItem]))
|
||||
self.assertFalse(Order.user_cart_has_items(self.user, [PaidCourseRegistration]))
|
||||
|
||||
def test_cart_clear(self):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
@@ -189,7 +191,7 @@ class OrderTest(ModuleStoreTestCase):
|
||||
def test_purchase_item_email_smtp_failure(self, error_logger):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
|
||||
with patch('shoppingcart.models.send_mail', side_effect=smtplib.SMTPException):
|
||||
with patch('shoppingcart.models.EmailMessage.send', side_effect=smtplib.SMTPException):
|
||||
cart.purchase()
|
||||
self.assertTrue(error_logger.called)
|
||||
|
||||
@@ -326,6 +328,15 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(self.cart.total_cost, self.cost)
|
||||
|
||||
def test_cart_type_business(self):
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.purchase()
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
# check that the registration codes are generated against the order
|
||||
self.assertEqual(len(CourseRegistrationCode.objects.filter(order=self.cart)), item.qty)
|
||||
|
||||
def test_add_with_default_mode(self):
|
||||
"""
|
||||
Tests add_to_cart where the mode specified in the argument is NOT in the database
|
||||
@@ -341,6 +352,31 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
self.assertEqual(self.cart.total_cost, 0)
|
||||
self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_key))
|
||||
|
||||
course_reg_code_item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2, mode_slug="DNE")
|
||||
|
||||
self.assertEqual(course_reg_code_item.unit_cost, 0)
|
||||
self.assertEqual(course_reg_code_item.line_cost, 0)
|
||||
self.assertEqual(course_reg_code_item.mode, "honor")
|
||||
self.assertEqual(course_reg_code_item.user, self.user)
|
||||
self.assertEqual(course_reg_code_item.status, "cart")
|
||||
self.assertEqual(self.cart.total_cost, 0)
|
||||
self.assertTrue(CourseRegCodeItem.contained_in_order(self.cart, self.course_key))
|
||||
|
||||
def test_add_course_reg_item_with_no_course_item(self):
|
||||
fake_course_id = CourseLocator(org="edx", course="fake", run="course")
|
||||
with self.assertRaises(CourseDoesNotExistException):
|
||||
CourseRegCodeItem.add_to_order(self.cart, fake_course_id, 2)
|
||||
|
||||
def test_course_reg_item_already_in_cart(self):
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
with self.assertRaises(ItemAlreadyInCartException):
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
|
||||
def test_course_reg_item_already_enrolled_in_course(self):
|
||||
CourseEnrollment.enroll(self.user, self.course_key)
|
||||
with self.assertRaises(AlreadyEnrolledInCourseException):
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
|
||||
def test_purchased_callback(self):
|
||||
reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.cart.purchase()
|
||||
@@ -382,6 +418,12 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
reg1.purchased_callback()
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
|
||||
course_reg_code_item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
course_reg_code_item.course_id = CourseLocator(org="changed1", course="forsome1", run="reason1")
|
||||
course_reg_code_item.save()
|
||||
with self.assertRaises(PurchasedCallbackException):
|
||||
course_reg_code_item.purchased_callback()
|
||||
|
||||
def test_user_cart_has_both_items(self):
|
||||
"""
|
||||
This test exists b/c having both CertificateItem and PaidCourseRegistration in an order used to break
|
||||
|
||||
@@ -13,7 +13,8 @@ from django.test.utils import override_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from shoppingcart.models import (Order, CertificateItem, PaidCourseRegistration, PaidCourseRegistrationAnnotation)
|
||||
from shoppingcart.models import (Order, CertificateItem, PaidCourseRegistration, PaidCourseRegistrationAnnotation,
|
||||
CourseRegCodeItemAnnotation)
|
||||
from shoppingcart.views import initialize_report
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
@@ -203,6 +204,8 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
|
||||
course_mode2.save()
|
||||
self.annotation = PaidCourseRegistrationAnnotation(course_id=self.course_key, annotation=self.TEST_ANNOTATION)
|
||||
self.annotation.save()
|
||||
self.course_reg_code_annotation = CourseRegCodeItemAnnotation(course_id=self.course_key, annotation=self.TEST_ANNOTATION)
|
||||
self.course_reg_code_annotation.save()
|
||||
self.cart = Order.get_cart_for_user(self.user)
|
||||
self.reg = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.cert_item = CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified')
|
||||
@@ -269,3 +272,9 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
|
||||
Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation
|
||||
"""
|
||||
self.assertEqual(unicode(self.annotation), u'{} : {}'.format(self.course_key.to_deprecated_string(), self.TEST_ANNOTATION))
|
||||
|
||||
def test_courseregcodeitemannotationannotation_unicode(self):
|
||||
"""
|
||||
Fill in gap in test coverage. __unicode__ method of CourseRegCodeItemAnnotation
|
||||
"""
|
||||
self.assertEqual(unicode(self.course_reg_code_annotation), u'{} : {}'.format(self.course_key.to_deprecated_string(), self.TEST_ANNOTATION))
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""
|
||||
Tests for Shopping Cart views
|
||||
"""
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
from decimal import Decimal
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.conf import settings
|
||||
@@ -14,6 +12,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.core import mail
|
||||
|
||||
from django.core.cache import cache
|
||||
from pytz import UTC
|
||||
@@ -28,7 +27,7 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from shoppingcart.views import _can_download_report, _get_date_from_str
|
||||
from shoppingcart.models import (
|
||||
Order, CertificateItem, PaidCourseRegistration,
|
||||
Order, CertificateItem, PaidCourseRegistration, CourseRegCodeItem,
|
||||
Coupon, CourseRegistrationCode, RegistrationCodeRedemption,
|
||||
DonationConfiguration
|
||||
)
|
||||
@@ -41,6 +40,8 @@ from shoppingcart.processors import render_purchase_form_html
|
||||
from shoppingcart.admin import SoftDeleteCouponAdmin
|
||||
from shoppingcart.views import initialize_report
|
||||
from shoppingcart.tests.payment_fake import PaymentFakeView
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
|
||||
def mock_render_purchase_form_html(*args, **kwargs):
|
||||
@@ -133,6 +134,30 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()]))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_billing_details(self):
|
||||
billing_url = reverse('billing_details')
|
||||
self.login_user()
|
||||
|
||||
# page not found error because order_type is not business
|
||||
resp = self.client.get(billing_url)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
#chagne the order_type to business
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
resp = self.client.get(billing_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
data = {'company_name': 'Test Company', 'company_contact_name': 'JohnDoe',
|
||||
'company_contact_email': 'john@est.com', 'recipient_name': 'Mocker',
|
||||
'recipient_email': 'mock@germ.com', 'company_address_line_1': 'DC Street # 1',
|
||||
'company_address_line_2': '',
|
||||
'company_city': 'DC', 'company_state': 'NY', 'company_zip': '22003', 'company_country': 'US',
|
||||
'customer_reference_number': 'PO#23'}
|
||||
|
||||
resp = self.client.post(billing_url, data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_add_course_to_cart_already_in_cart(self):
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.login_user()
|
||||
@@ -148,6 +173,120 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertIn("Discount does not exist against code '{0}'.".format(non_existing_code), resp.content)
|
||||
|
||||
def test_valid_qty_greater_then_one_and_purchase_type_should_business(self):
|
||||
qty = 2
|
||||
item = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = json.loads(resp.content)
|
||||
self.assertEqual(data['total_cost'], item.unit_cost * qty)
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
self.assertEqual(cart.order_type, 'business')
|
||||
|
||||
def test_in_valid_qty_case(self):
|
||||
# invalid quantity, Quantity must be between 1 and 1000.
|
||||
qty = 0
|
||||
item = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn("Quantity must be between 1 and 1000.", resp.content)
|
||||
|
||||
# invalid quantity, Quantity must be an integer.
|
||||
qty = 'abcde'
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn("Quantity must be an integer.", resp.content)
|
||||
|
||||
# invalid quantity, Quantity is not present in request
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn("Quantity must be between 1 and 1000.", resp.content)
|
||||
|
||||
def test_valid_qty_but_item_not_found(self):
|
||||
qty = 2
|
||||
item_id = '-1'
|
||||
self.login_user()
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item_id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual('Order item does not exist.', resp.content)
|
||||
|
||||
# now testing the case if item id not found in request,
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'qty': qty})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual('Order item not found in request.', resp.content)
|
||||
|
||||
def test_purchase_type_should_be_personal_when_qty_is_one(self):
|
||||
qty = 1
|
||||
item = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = json.loads(resp.content)
|
||||
self.assertEqual(data['total_cost'], item.unit_cost * 1)
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
self.assertEqual(cart.order_type, 'personal')
|
||||
|
||||
def test_purchase_type_on_removing_item_and_cart_has_item_with_qty_one(self):
|
||||
qty = 5
|
||||
self.add_course_to_user_cart(self.course_key)
|
||||
item2 = self.add_course_to_user_cart(self.testing_course.id)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item2.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
cart_items = cart.orderitem_set.all()
|
||||
test_flag = False
|
||||
for cartitem in cart_items:
|
||||
if cartitem.qty == 5:
|
||||
test_flag = True
|
||||
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': cartitem.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(test_flag)
|
||||
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
self.assertEqual(cart.order_type, 'personal')
|
||||
|
||||
def test_billing_details_btn_in_cart_when_qty_is_greater_than_one(self):
|
||||
qty = 5
|
||||
item = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
|
||||
self.assertIn("Billing Details", resp.content)
|
||||
|
||||
def test_purchase_type_should_be_personal_when_remove_all_items_from_cart(self):
|
||||
item1 = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item1.id, 'qty': 2})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
item2 = self.add_course_to_user_cart(self.testing_course.id)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item2.id, 'qty': 5})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
cart_items = cart.orderitem_set.all()
|
||||
test_flag = False
|
||||
for cartitem in cart_items:
|
||||
test_flag = True
|
||||
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': cartitem.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(test_flag)
|
||||
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
self.assertEqual(cart.order_type, 'personal')
|
||||
|
||||
def test_use_valid_coupon_code_and_qty_is_greater_than_one(self):
|
||||
qty = 5
|
||||
item = self.add_course_to_user_cart(self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = json.loads(resp.content)
|
||||
self.assertEqual(data['total_cost'], item.unit_cost * qty)
|
||||
|
||||
# use coupon code
|
||||
self.add_coupon(self.course_key, True, self.coupon_code)
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
|
||||
item = self.cart.orderitem_set.all().select_subclasses()[0]
|
||||
self.assertEquals(item.unit_cost * qty, 180)
|
||||
|
||||
def test_course_discount_invalid_reg_code(self):
|
||||
self.add_reg_code(self.course_key)
|
||||
self.add_course_to_user_cart(self.course_key)
|
||||
@@ -319,6 +458,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
info_log.assert_called_with(
|
||||
'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id))
|
||||
|
||||
@patch('shoppingcart.views.log.info')
|
||||
def test_reset_redemption_for_coupon(self, info_log):
|
||||
|
||||
self.add_coupon(self.course_key, True, self.coupon_code)
|
||||
reg_item = self.add_course_to_user_cart(self.course_key)
|
||||
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(reverse('shoppingcart.views.reset_code_redemption', args=[]))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
info_log.assert_called_with(
|
||||
'Coupon redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id))
|
||||
|
||||
@patch('shoppingcart.views.log.info')
|
||||
def test_reset_redemption_for_registration_code(self, info_log):
|
||||
|
||||
self.add_reg_code(self.course_key)
|
||||
reg_item = self.add_course_to_user_cart(self.course_key)
|
||||
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(reverse('shoppingcart.views.reset_code_redemption', args=[]))
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
info_log.assert_called_with(
|
||||
'Registration code redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id))
|
||||
|
||||
@patch('shoppingcart.views.log.info')
|
||||
def test_existing_reg_code_redemption_on_removing_item(self, info_log):
|
||||
|
||||
@@ -474,14 +643,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
((purchase_form_arg_cart,), _) = form_mock.call_args
|
||||
((purchase_form_arg_cart,), _) = form_mock.call_args # pylint: disable=W0621
|
||||
purchase_form_arg_cart_items = purchase_form_arg_cart.orderitem_set.all().select_subclasses()
|
||||
self.assertIn(reg_item, purchase_form_arg_cart_items)
|
||||
self.assertIn(cert_item, purchase_form_arg_cart_items)
|
||||
self.assertEqual(len(purchase_form_arg_cart_items), 2)
|
||||
|
||||
((template, context), _) = render_mock.call_args
|
||||
self.assertEqual(template, 'shoppingcart/list.html')
|
||||
self.assertEqual(template, 'shoppingcart/shopping_cart.html')
|
||||
self.assertEqual(len(context['shoppingcart_items']), 2)
|
||||
self.assertEqual(context['amount'], 80)
|
||||
self.assertIn("80.00", context['form_html'])
|
||||
@@ -626,7 +795,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
|
||||
self.assertIn('Check Out', resp.content)
|
||||
self.assertIn('Payment', resp.content)
|
||||
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
|
||||
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id]))
|
||||
@@ -665,13 +834,58 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
self.assertIn('FirstNameTesting123', resp.content)
|
||||
self.assertIn('80.00', resp.content)
|
||||
|
||||
((template, context), _) = render_mock.call_args
|
||||
((template, context), _) = render_mock.call_args # pylint: disable=W0621
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item, context['order_items'])
|
||||
self.assertIn(cert_item, context['order_items'])
|
||||
self.assertIn(reg_item, context['shoppingcart_items'][0])
|
||||
self.assertIn(cert_item, context['shoppingcart_items'][1])
|
||||
self.assertFalse(context['any_refunds'])
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_courseregcode_item_total_price(self):
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
|
||||
self.assertEquals(CourseRegCodeItem.get_total_amount_of_purchased_item(self.course_key), 80)
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_show_receipt_success_with_order_type_business(self):
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
reg_item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.add_billing_details(company_name='T1Omega', company_contact_name='C1',
|
||||
company_contact_email='test@t1.com', recipient_email='test@t2.com')
|
||||
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
|
||||
|
||||
# mail is sent to these emails recipient_email, company_contact_email, order.user.email
|
||||
self.assertEquals(len(mail.outbox), 3)
|
||||
|
||||
self.login_user()
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id]))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# when order_type = 'business' the user is not enrolled in the
|
||||
# course but presented with the enrollment links
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.cart.user, self.course_key))
|
||||
self.assertIn('FirstNameTesting123', resp.content)
|
||||
self.assertIn('80.00', resp.content)
|
||||
# check for the enrollment codes content
|
||||
self.assertIn('Please send each professional one of these unique registration codes to enroll into the course.', resp.content)
|
||||
|
||||
((template, context), _) = render_mock.call_args # pylint: disable=W0621
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item, context['shoppingcart_items'][0])
|
||||
self.assertIn(self.cart.purchase_time.strftime("%B %d, %Y"), resp.content)
|
||||
self.assertIn(self.cart.company_name, resp.content)
|
||||
self.assertIn(self.cart.company_contact_name, resp.content)
|
||||
self.assertIn(self.cart.company_contact_email, resp.content)
|
||||
self.assertIn(self.cart.recipient_email, resp.content)
|
||||
self.assertIn("Invoice #{order_id}".format(order_id=self.cart.id), resp.content)
|
||||
self.assertIn('You have successfully purchased <b>{total_registration_codes} course registration codes'
|
||||
.format(total_registration_codes=context['total_registration_codes']), resp.content)
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_show_receipt_success_with_upgrade(self):
|
||||
|
||||
@@ -705,8 +919,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item, context['order_items'])
|
||||
self.assertIn(cert_item, context['order_items'])
|
||||
self.assertIn(reg_item, context['shoppingcart_items'][0])
|
||||
self.assertIn(cert_item, context['shoppingcart_items'][1])
|
||||
self.assertFalse(context['any_refunds'])
|
||||
|
||||
course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key)
|
||||
@@ -736,8 +950,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
((template, context), _tmp) = render_mock.call_args
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item, context['order_items'])
|
||||
self.assertIn(cert_item, context['order_items'])
|
||||
self.assertIn(reg_item, context['shoppingcart_items'][0])
|
||||
self.assertIn(cert_item, context['shoppingcart_items'][1])
|
||||
self.assertTrue(context['any_refunds'])
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
@@ -869,6 +1083,12 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
|
||||
response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'})
|
||||
self.assertTrue("You've clicked a link for an enrollment code that has already been used." in response.content)
|
||||
|
||||
#now check the response of the dashboard page
|
||||
dashboard_url = reverse('dashboard')
|
||||
response = self.client.get(dashboard_url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue(self.course.display_name, response.content)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@ddt.ddt
|
||||
|
||||
@@ -17,6 +17,9 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']:
|
||||
url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'),
|
||||
url(r'^register/redeem/(?P<registration_code>[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'),
|
||||
url(r'^use_code/$', 'use_code'),
|
||||
url(r'^update_user_cart/$', 'update_user_cart'),
|
||||
url(r'^reset_code_redemption/$', 'reset_code_redemption'),
|
||||
url(r'^billing_details/$', 'billing_details', name='billing_details'),
|
||||
url(r'^register_courses/$', 'register_courses'),
|
||||
)
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ from django.http import (
|
||||
HttpResponseBadRequest, HttpResponseForbidden, Http404
|
||||
)
|
||||
from django.utils.translation import ugettext as _
|
||||
from util.json_request import JsonResponse
|
||||
from django.views.decorators.http import require_POST, require_http_methods
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from microsite_configuration import microsite
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from microsite_configuration import microsite
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
@@ -31,7 +32,8 @@ from .exceptions import (
|
||||
MultipleCouponsNotAllowedException, InvalidCartItem
|
||||
)
|
||||
from .models import (
|
||||
Order, PaidCourseRegistration, OrderItem, Coupon,
|
||||
Order, OrderTypes,
|
||||
PaidCourseRegistration, OrderItem, Coupon, CourseRegCodeItem,
|
||||
CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption,
|
||||
Donation, DonationConfiguration
|
||||
)
|
||||
@@ -39,6 +41,7 @@ from .processors import (
|
||||
process_postpay_callback, render_purchase_form_html,
|
||||
get_signed_purchase_params, get_purchase_endpoint
|
||||
)
|
||||
|
||||
import json
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
@@ -90,22 +93,68 @@ def add_course_to_cart(request, course_id):
|
||||
return HttpResponse(_("Course added to cart."))
|
||||
|
||||
|
||||
@login_required
|
||||
def update_user_cart(request):
|
||||
"""
|
||||
when user change the number-of-students from the UI then
|
||||
this method Update the corresponding qty field in OrderItem model and update the order_type in order model.
|
||||
"""
|
||||
try:
|
||||
qty = int(request.POST.get('qty', -1))
|
||||
except ValueError:
|
||||
log.exception('Quantity must be an integer.')
|
||||
return HttpResponseBadRequest('Quantity must be an integer.')
|
||||
|
||||
if not 1 <= qty <= 1000:
|
||||
log.warning('Quantity must be between 1 and 1000.')
|
||||
return HttpResponseBadRequest('Quantity must be between 1 and 1000.')
|
||||
|
||||
item_id = request.POST.get('ItemId', None)
|
||||
if item_id:
|
||||
try:
|
||||
item = OrderItem.objects.get(id=item_id, status='cart')
|
||||
except OrderItem.DoesNotExist:
|
||||
log.exception('Cart OrderItem id={0} DoesNotExist'.format(item_id))
|
||||
return HttpResponseNotFound('Order item does not exist.')
|
||||
|
||||
item.qty = qty
|
||||
item.save()
|
||||
item.order.update_order_type()
|
||||
total_cost = item.order.total_cost
|
||||
return JsonResponse({"total_cost": total_cost}, 200)
|
||||
|
||||
return HttpResponseBadRequest('Order item not found in request.')
|
||||
|
||||
|
||||
@login_required
|
||||
def show_cart(request):
|
||||
"""
|
||||
This view shows cart items.
|
||||
"""
|
||||
cart = Order.get_cart_for_user(request.user)
|
||||
total_cost = cart.total_cost
|
||||
cart_items = cart.orderitem_set.all()
|
||||
cart_items = cart.orderitem_set.all().select_subclasses()
|
||||
shoppingcart_items = []
|
||||
for cart_item in cart_items:
|
||||
course_key = getattr(cart_item, 'course_id')
|
||||
if course_key:
|
||||
course = get_course_by_id(course_key, depth=0)
|
||||
shoppingcart_items.append((cart_item, course))
|
||||
|
||||
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
|
||||
|
||||
callback_url = request.build_absolute_uri(
|
||||
reverse("shoppingcart.views.postpay_callback")
|
||||
)
|
||||
form_html = render_purchase_form_html(cart, callback_url=callback_url)
|
||||
context = {
|
||||
'shoppingcart_items': cart_items,
|
||||
'order': cart,
|
||||
'shoppingcart_items': shoppingcart_items,
|
||||
'amount': total_cost,
|
||||
'site_name': site_name,
|
||||
'form_html': form_html,
|
||||
}
|
||||
return render_to_response("shoppingcart/list.html", context)
|
||||
return render_to_response("shoppingcart/shopping_cart.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -127,22 +176,26 @@ def clear_cart(request):
|
||||
|
||||
@login_required
|
||||
def remove_item(request):
|
||||
"""
|
||||
This will remove an item from the user cart and also delete the corresponding coupon codes redemption.
|
||||
"""
|
||||
item_id = request.REQUEST.get('id', '-1')
|
||||
try:
|
||||
item = OrderItem.objects.get(id=item_id, status='cart')
|
||||
|
||||
items = OrderItem.objects.filter(id=item_id, status='cart').select_subclasses()
|
||||
|
||||
if not len(items):
|
||||
log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id))
|
||||
else:
|
||||
item = items[0]
|
||||
if item.user == request.user:
|
||||
order_item_course_id = None
|
||||
if hasattr(item, 'paidcourseregistration'):
|
||||
order_item_course_id = item.paidcourseregistration.course_id
|
||||
order_item_course_id = getattr(item, 'course_id')
|
||||
item.delete()
|
||||
log.info('order item {0} removed for user {1}'.format(item_id, request.user))
|
||||
remove_code_redemption(order_item_course_id, item_id, item, request.user)
|
||||
item.order.update_order_type()
|
||||
|
||||
except OrderItem.DoesNotExist:
|
||||
log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id))
|
||||
return HttpResponse('OK')
|
||||
|
||||
|
||||
def remove_code_redemption(order_item_course_id, item_id, item, user):
|
||||
"""
|
||||
If an item removed from shopping cart then we will remove
|
||||
@@ -159,16 +212,30 @@ def remove_code_redemption(order_item_course_id, item_id, item, user):
|
||||
log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
|
||||
.format(coupon_redemption.coupon.code, user, item_id))
|
||||
except CouponRedemption.DoesNotExist:
|
||||
try:
|
||||
# Try to remove redemption information of registration code, If exist.
|
||||
reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id)
|
||||
except RegistrationCodeRedemption.DoesNotExist:
|
||||
log.debug('Code redemption does not exist for order item id={0}.'.format(item_id))
|
||||
else:
|
||||
if order_item_course_id == reg_code_redemption.registration_code.course_id:
|
||||
reg_code_redemption.delete()
|
||||
log.info('Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"'
|
||||
.format(reg_code_redemption.registration_code.code, user, item_id))
|
||||
pass
|
||||
|
||||
try:
|
||||
# Try to remove redemption information of registration code, If exist.
|
||||
reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id)
|
||||
except RegistrationCodeRedemption.DoesNotExist:
|
||||
log.debug('Code redemption does not exist for order item id={0}.'.format(item_id))
|
||||
else:
|
||||
if order_item_course_id == reg_code_redemption.registration_code.course_id:
|
||||
reg_code_redemption.delete()
|
||||
log.info('Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"'
|
||||
.format(reg_code_redemption.registration_code.code, user, item_id))
|
||||
|
||||
|
||||
@login_required
|
||||
def reset_code_redemption(request):
|
||||
"""
|
||||
This method reset the code redemption from user cart items.
|
||||
"""
|
||||
cart = Order.get_cart_for_user(request.user)
|
||||
cart.reset_cart_items_prices()
|
||||
CouponRedemption.delete_coupon_redemption(request.user, cart)
|
||||
RegistrationCodeRedemption.delete_registration_redemption(request.user, cart)
|
||||
return HttpResponse('reset')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -448,6 +515,49 @@ def postpay_callback(request):
|
||||
return render_to_response('shoppingcart/error.html', {'order': result['order'],
|
||||
'error_html': result['error_html']})
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@login_required
|
||||
def billing_details(request):
|
||||
"""
|
||||
This is the view for capturing additional billing details
|
||||
in case of the business purchase workflow.
|
||||
"""
|
||||
|
||||
cart = Order.get_cart_for_user(request.user)
|
||||
cart_items = cart.orderitem_set.all()
|
||||
|
||||
if getattr(cart, 'order_type') != OrderTypes.BUSINESS:
|
||||
raise Http404('Page not found!')
|
||||
|
||||
if request.method == "GET":
|
||||
callback_url = request.build_absolute_uri(
|
||||
reverse("shoppingcart.views.postpay_callback")
|
||||
)
|
||||
form_html = render_purchase_form_html(cart, callback_url=callback_url)
|
||||
total_cost = cart.total_cost
|
||||
context = {
|
||||
'shoppingcart_items': cart_items,
|
||||
'amount': total_cost,
|
||||
'form_html': form_html,
|
||||
'site_name': microsite.get_value('SITE_NAME', settings.SITE_NAME),
|
||||
}
|
||||
return render_to_response("shoppingcart/billing_details.html", context)
|
||||
elif request.method == "POST":
|
||||
company_name = request.POST.get("company_name", "")
|
||||
company_contact_name = request.POST.get("company_contact_name", "")
|
||||
company_contact_email = request.POST.get("company_contact_email", "")
|
||||
recipient_name = request.POST.get("recipient_name", "")
|
||||
recipient_email = request.POST.get("recipient_email", "")
|
||||
customer_reference_number = request.POST.get("customer_reference_number", "")
|
||||
|
||||
cart.add_billing_details(company_name, company_contact_name, company_contact_email, recipient_name,
|
||||
recipient_email, customer_reference_number)
|
||||
return JsonResponse({
|
||||
'response': _('success')
|
||||
}) # status code 200: OK by default
|
||||
|
||||
|
||||
@login_required
|
||||
def show_receipt(request, ordernum):
|
||||
"""
|
||||
@@ -464,21 +574,20 @@ def show_receipt(request, ordernum):
|
||||
raise Http404('Order not found!')
|
||||
|
||||
order_items = OrderItem.objects.filter(order=order).select_subclasses()
|
||||
shoppingcart_items = []
|
||||
course_names_list = []
|
||||
for order_item in order_items:
|
||||
course_key = getattr(order_item, 'course_id')
|
||||
if course_key:
|
||||
course = get_course_by_id(course_key, depth=0)
|
||||
shoppingcart_items.append((order_item, course))
|
||||
course_names_list.append(course.display_name)
|
||||
|
||||
appended_course_names = ", ".join(course_names_list)
|
||||
any_refunds = any(i.status == "refunded" for i in order_items)
|
||||
receipt_template = 'shoppingcart/receipt.html'
|
||||
__, instructions = order.generate_receipt_instructions()
|
||||
# we want to have the ability to override the default receipt page when
|
||||
# there is only one item in the order
|
||||
context = {
|
||||
'order': order,
|
||||
'order_items': order_items,
|
||||
'any_refunds': any_refunds,
|
||||
'instructions': instructions,
|
||||
}
|
||||
|
||||
if order_items.count() == 1:
|
||||
receipt_template = order_items[0].single_item_receipt_template
|
||||
context.update(order_items[0].single_item_receipt_context)
|
||||
order_type = getattr(order, 'order_type')
|
||||
|
||||
# Only orders where order_items.count() == 1 might be attempting to upgrade
|
||||
attempting_upgrade = request.session.get('attempting_upgrade', False)
|
||||
@@ -487,6 +596,39 @@ def show_receipt(request, ordernum):
|
||||
course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED)
|
||||
request.session['attempting_upgrade'] = False
|
||||
|
||||
recipient_list = []
|
||||
registration_codes = None
|
||||
total_registration_codes = None
|
||||
recipient_list.append(getattr(order.user, 'email'))
|
||||
if order_type == OrderTypes.BUSINESS:
|
||||
registration_codes = CourseRegistrationCode.objects.filter(order=order)
|
||||
total_registration_codes = registration_codes.count()
|
||||
if order.company_contact_email:
|
||||
recipient_list.append(order.company_contact_email)
|
||||
if order.recipient_email:
|
||||
recipient_list.append(order.recipient_email)
|
||||
|
||||
appended_recipient_emails = ", ".join(recipient_list)
|
||||
|
||||
context = {
|
||||
'order': order,
|
||||
'shoppingcart_items': shoppingcart_items,
|
||||
'any_refunds': any_refunds,
|
||||
'instructions': instructions,
|
||||
'site_name': microsite.get_value('SITE_NAME', settings.SITE_NAME),
|
||||
'order_type': order_type,
|
||||
'appended_course_names': appended_course_names,
|
||||
'appended_recipient_emails': appended_recipient_emails,
|
||||
'total_registration_codes': total_registration_codes,
|
||||
'registration_codes': registration_codes,
|
||||
'order_purchase_date': order.purchase_time.strftime("%B %d, %Y"),
|
||||
}
|
||||
# we want to have the ability to override the default receipt page when
|
||||
# there is only one item in the order
|
||||
if order_items.count() == 1:
|
||||
receipt_template = order_items[0].single_item_receipt_template
|
||||
context.update(order_items[0].single_item_receipt_context)
|
||||
|
||||
return render_to_response(receipt_template, context)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class ECommerce
|
||||
# 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']'")
|
||||
@$active_company_name = @$section.find("input[name='active_company_name']'")
|
||||
@$spent_company_name = @$section.find('input[name="spent_company_name"]')
|
||||
@@ -35,6 +36,10 @@ class ECommerce
|
||||
url += '/csv'
|
||||
location.href = url
|
||||
|
||||
@$list_order_sale_csv_btn.click (e) =>
|
||||
url = @$list_order_sale_csv_btn.data 'endpoint'
|
||||
location.href = url
|
||||
|
||||
@$download_coupon_codes.click (e) =>
|
||||
url = @$download_coupon_codes.data 'endpoint'
|
||||
location.href = url
|
||||
|
||||
@@ -55,6 +55,15 @@ span {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-dark-grey {
|
||||
color: $dark-gray1;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@@ -421,3 +421,14 @@ $header-sans-serif: 'Open Sans', Arial, Helvetica, sans-serif;
|
||||
// SPLINT: colors
|
||||
|
||||
$msg-bg: $action-primary-bg;
|
||||
|
||||
// New Shopping Cart
|
||||
|
||||
$dark-gray1: #4a4a4a;
|
||||
$light-gray1: #f2f2f2;
|
||||
$light-gray2: #ababab;
|
||||
$dark-gray2: #979797;
|
||||
$blue1: #4A90E2;
|
||||
$blue2: #00A1E5;
|
||||
$green1: #61A12E;
|
||||
$red1: #D0021B;
|
||||
|
||||
@@ -1144,6 +1144,9 @@ input[name="subject"] {
|
||||
|
||||
}
|
||||
#e-commerce{
|
||||
input[name='list-order-sale-csv'] {
|
||||
margin-right: 14px;
|
||||
}
|
||||
input {
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.3em;
|
||||
@@ -1292,22 +1295,20 @@ input[name="subject"] {
|
||||
width: 650px;
|
||||
margin-left: -325px;
|
||||
border-radius: 2px;
|
||||
input[type="submit"]#update_coupon_button{
|
||||
@include button(simple, $blue);
|
||||
@extend .button-reset;
|
||||
}
|
||||
input[type="submit"]#add_coupon_button{
|
||||
input[type="button"]#update_coupon_button, input[type="button"]#add_coupon_button,
|
||||
input[type="button"]#set_course_button {
|
||||
@include button(simple, $blue);
|
||||
@extend .button-reset;
|
||||
display: block;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
input[name="generate-registration-codes-csv"]{
|
||||
@include button(simple, $blue);
|
||||
@extend .button-reset;
|
||||
}
|
||||
input[type="submit"]#set_course_button{
|
||||
@include button(simple, $blue);
|
||||
@extend .button-reset;
|
||||
}
|
||||
.modal-form-error {
|
||||
box-shadow: inset 0 -1px 2px 0 #f3d9db;
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
font-size: 1.5em;
|
||||
color: $base-font-color;
|
||||
}
|
||||
|
||||
|
||||
.cart-table {
|
||||
width: 100%;
|
||||
tr:nth-child(even){
|
||||
@@ -66,7 +66,7 @@
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid $border-color-1;
|
||||
|
||||
|
||||
&.qty {
|
||||
width: 100px;
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cart-items {
|
||||
td {
|
||||
padding: 10px 0px;
|
||||
@@ -115,7 +115,7 @@
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cart-totals {
|
||||
td {
|
||||
&.cart-total-cost {
|
||||
@@ -127,10 +127,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
table.order-receipt {
|
||||
width: 100%;
|
||||
|
||||
|
||||
.order-number {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 25px 0 15px 0;
|
||||
|
||||
|
||||
&.qty {
|
||||
width: 50px;
|
||||
}
|
||||
@@ -210,7 +210,7 @@
|
||||
}
|
||||
}
|
||||
.enrollment-text {
|
||||
color: #4A4A46;
|
||||
color: #9b9b93;
|
||||
font-family: 'Open Sans',Verdana,Geneva,sans;
|
||||
line-height: normal;
|
||||
a {
|
||||
@@ -264,4 +264,655 @@
|
||||
text-shadow: 0 1px 0 #00A1E5;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shopping-cart{
|
||||
a.blue{
|
||||
display: inline-block;
|
||||
background: $blue2;
|
||||
color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 3px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
margin: 10px 0px 20px;
|
||||
&:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.relative{
|
||||
position: relative;
|
||||
}
|
||||
input[type="text"], input[type="email"] , select{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-style: normal;
|
||||
border: 2px solid $dark-gray2;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
width: 260px;
|
||||
font-size: 16px;
|
||||
&:focus{
|
||||
border-color: $dark-gray2;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
&.error{
|
||||
border-color: $red1;
|
||||
}
|
||||
}
|
||||
.hidden{display: none;}
|
||||
.show{display: inline-block;}
|
||||
h1{
|
||||
font-size: 24px;
|
||||
color: $dark-gray1;
|
||||
text-align: left;
|
||||
padding: 15px 0px;
|
||||
margin: 10px 0 0 0;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
ul.steps{
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
border-top: 3px solid $light-gray1;
|
||||
border-bottom: 3px solid $light-gray1;
|
||||
li{
|
||||
display: inline-block;
|
||||
padding: 26px 30px;
|
||||
margin: 0px 30px;
|
||||
font-size: 20px;
|
||||
font-weight: 100;
|
||||
position: relative;
|
||||
color: $dark-gray1;
|
||||
&.active{font-weight: 400; border-bottom: 3px solid $light-gray1;}
|
||||
&:first-child {padding-left: 30px;margin-left: 0;}
|
||||
&:last-child {
|
||||
padding-right: 30px;margin-right: 0;
|
||||
&:after{display: none;}
|
||||
}
|
||||
&:after{
|
||||
content: "\f178";
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
right: -40px;
|
||||
color: #ddd;
|
||||
font-weight: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
hr{border-top: 1px solid $dark-gray2;}
|
||||
.user-data{
|
||||
margin: 20px 0px;
|
||||
.image{
|
||||
width: 220px;
|
||||
float: left;
|
||||
img{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.data-input{
|
||||
width: calc(100% - 245px);
|
||||
float: left;
|
||||
margin-left: 25px;
|
||||
h3, h3 span{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray2;
|
||||
padding: 0;
|
||||
}
|
||||
h1, h1 span{
|
||||
font-size: 24px;
|
||||
color: $dark-gray1;
|
||||
padding: 0 0 10px 0;
|
||||
text-transform: capitalize;
|
||||
span{font-size: 16px;}
|
||||
}
|
||||
hr{border-top: 1px solid $dark-gray2;}
|
||||
.three-col{
|
||||
.col-1{
|
||||
width: 450px;
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray2;
|
||||
.price{
|
||||
span{
|
||||
color: $dark-gray1;
|
||||
font-size: 24px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
&.green{color: $green1;}
|
||||
.line-through{text-decoration: line-through;}
|
||||
}
|
||||
}
|
||||
.col-2{
|
||||
width: 350px;
|
||||
float: left;
|
||||
line-height: 44px;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray2;
|
||||
.numbers-row{
|
||||
position: relative;
|
||||
label{
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray2;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
.counter{
|
||||
margin-left: 25px;
|
||||
border-radius: 3px;
|
||||
padding: 6px 30px 6px 10px;
|
||||
display: inline-block;
|
||||
border: 2px solid $dark-gray2;
|
||||
input[type="text"]{
|
||||
width: 75px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: #666;
|
||||
font-size: 25px;
|
||||
font-style: normal;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-weight: 600;
|
||||
padding: 8px 0;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.button{
|
||||
position: absolute;
|
||||
background: none;
|
||||
margin-left: -30px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
height: 17px;
|
||||
i{
|
||||
color: $dark-gray2;
|
||||
font-size: 24px;
|
||||
span{display: none;}
|
||||
}
|
||||
&.inc{top: 9px;}
|
||||
&.dec{top: 30px;height: 22px;}
|
||||
}
|
||||
&.disabled{
|
||||
.counter{
|
||||
border: 2px solid #CCCCCC;
|
||||
&:hover{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
input{
|
||||
color: #CCC;
|
||||
}
|
||||
}
|
||||
.button{
|
||||
i{
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
.updateBtn{
|
||||
display: inline-block;
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
padding: 25px 35px 25px 0;
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
span.error-text{
|
||||
display: block;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
.disable-numeric-counter{
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.col-3{
|
||||
width: 100px;
|
||||
float: right;
|
||||
a.btn-remove{
|
||||
float: right;
|
||||
opacity: 0.8;
|
||||
i{
|
||||
color: $dark-gray2;
|
||||
font-size: 24PX;
|
||||
line-height: 40px;
|
||||
}
|
||||
&:hover{text-decoration: none;opacity: 1;}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.discount{
|
||||
border-bottom: 2px solid $light-gray1;
|
||||
border-top: 2px solid $light-gray1;
|
||||
margin: 20px 0;
|
||||
padding: 17px 20px 15px;
|
||||
min-height: 45px;
|
||||
.code-text{
|
||||
a{
|
||||
color: $blue1;
|
||||
font-size: 18px;
|
||||
text-transform: lowercase;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
padding: 10px 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
span{
|
||||
display: inline-block;
|
||||
padding: 9px 0px;
|
||||
b{
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
padding-left: 20px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-input{
|
||||
display: inline-block;
|
||||
input[type="text"]{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-style: normal;
|
||||
border: 2px solid $dark-gray2;
|
||||
height: auto;
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
width: 260px;
|
||||
&:focus{
|
||||
border-color: $dark-gray2;
|
||||
box-shadow: none;
|
||||
}
|
||||
&.error{
|
||||
border-color: $red1;
|
||||
}
|
||||
}
|
||||
.error-text{
|
||||
color: $red1;
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
input[type="submit"]{
|
||||
padding: 9px 35px;
|
||||
}
|
||||
}
|
||||
.code-applied{
|
||||
display: inline-block;
|
||||
.green{
|
||||
color: $green1;
|
||||
font-weight: 600;
|
||||
margin-right: 20px;
|
||||
}
|
||||
input[type="submit"]{
|
||||
padding: 9px 35px;
|
||||
background: white;
|
||||
border: 2px solid $dark-gray2;
|
||||
color: $dark-gray2;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
&:hover{
|
||||
background: white;
|
||||
color: $dark-gray1;
|
||||
border: 2px solid $dark-gray2;
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type="submit"]{
|
||||
width: auto;
|
||||
padding: 7px 20px;
|
||||
height: auto;
|
||||
float: none;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 600;
|
||||
&:hover{
|
||||
background: #1F8FC2;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.col-two{
|
||||
overflow: hidden;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f2f2f2;
|
||||
.row-inside {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: 10px 0;
|
||||
b{
|
||||
font-size: 14px;
|
||||
width: 190px;
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
vertical-align: top;
|
||||
}
|
||||
label{
|
||||
width: 300px;
|
||||
margin: 0px;
|
||||
display: inline-block;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
.col-1{
|
||||
width: 35%;
|
||||
float: left;
|
||||
span.radio-group{
|
||||
display: inline-block;
|
||||
border: 2px solid #979797;
|
||||
border-radius: 3px;
|
||||
margin: 10px 0;
|
||||
margin-left: 5px;
|
||||
&:first-child{
|
||||
margin-left: 15px;
|
||||
}
|
||||
&.blue{
|
||||
border-color: $blue2;
|
||||
label{
|
||||
color: $blue2;
|
||||
}
|
||||
}
|
||||
label{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
color: $dark-gray2;
|
||||
font-weight: 400;
|
||||
padding: 8px 15px 8px 6px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
input[type="radio"]{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.col-2{
|
||||
width: 65%;
|
||||
float: right;
|
||||
input[type="submit"]{
|
||||
width: auto;
|
||||
padding: 18px 60px 22px 30px;
|
||||
height: auto;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 600;
|
||||
margin-left: 15px;
|
||||
&#register{
|
||||
padding: 18px 30px;
|
||||
}
|
||||
}
|
||||
p{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
padding: 13px 0;
|
||||
text-align: right;
|
||||
}
|
||||
form{
|
||||
position: relative;
|
||||
}
|
||||
i.icon-caret-right{
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 25px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
label.pull-right{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-style: normal;
|
||||
text-align: right;
|
||||
padding: 10px 25px 10px;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
line-height: 20px;
|
||||
color: $dark-gray1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.disclaimer{
|
||||
color: $light-gray2;
|
||||
padding: 10px 0px;
|
||||
text-align: right;
|
||||
font-weight: 300;
|
||||
}
|
||||
h3{
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
padding: 30px 20px;
|
||||
color: $dark-gray1;
|
||||
}
|
||||
.billing-data{
|
||||
display: table;
|
||||
width: 100%;
|
||||
h3{
|
||||
padding: 12px 0px;
|
||||
color: $dark-gray1;
|
||||
font-size: 17px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.row{
|
||||
display: table-row;
|
||||
}
|
||||
.col-half{
|
||||
width: 45%;
|
||||
float: left;
|
||||
background: $light-gray1;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
min-height: 240px;
|
||||
&:nth-child(even){
|
||||
margin-left: 30px;
|
||||
}
|
||||
.data-group{
|
||||
margin-bottom: 15px;
|
||||
label{
|
||||
display: block;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
color: $dark-gray2;
|
||||
}
|
||||
input{width: 100%;margin-bottom: 5px;}
|
||||
&:nth-child(4n){
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.error-text{
|
||||
color: $red1;
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.gray-bg{
|
||||
background: $light-gray1;
|
||||
border-radius: 3px;
|
||||
padding: 20px 20px 20px 30px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
.message-left{
|
||||
float: left;
|
||||
line-height: 24px;
|
||||
color: $dark-gray1;
|
||||
b{
|
||||
text-transform: capitalize;
|
||||
}
|
||||
a.blue{
|
||||
margin:0 0 0 20px;
|
||||
i{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bordered-bar{
|
||||
border-bottom: 2px solid $light-gray1;
|
||||
border-top: 2px solid $light-gray1;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
h2{
|
||||
color: $dark-gray1;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
font-size: 17px;
|
||||
span{
|
||||
padding-left: 60px;
|
||||
text-transform: capitalize;
|
||||
.blue-link{
|
||||
color: $blue2;
|
||||
font-size: 14px;
|
||||
&:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pattern{
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding:20px;
|
||||
color: $dark-gray1;
|
||||
}
|
||||
hr.border{
|
||||
border-top: 2px solid $light-gray1;
|
||||
}
|
||||
.no-border{border: none !important; }
|
||||
table.course-receipt{
|
||||
width: 94%;
|
||||
margin: auto;
|
||||
margin-bottom: 27px;
|
||||
thead{
|
||||
th{
|
||||
color: $light-gray2;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid $dark-gray2;
|
||||
&:first-child{
|
||||
text-align: left;
|
||||
}
|
||||
&:last-child{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr{
|
||||
border-bottom: 1px solid $light-gray1;
|
||||
&:last-child{
|
||||
border-bottom: none;
|
||||
}
|
||||
td{
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
color: $dark-gray1;
|
||||
width: 33.33333%;
|
||||
|
||||
&:first-child{
|
||||
text-align: left;
|
||||
font-size: 18px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
&:last-child{
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty-cart{
|
||||
padding: 20px 0px;
|
||||
background: $light-gray1;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
margin: 20px 0px;
|
||||
h2{
|
||||
font-size: 24PX;
|
||||
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
color: #9b9b9b;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
text-transform: initial;
|
||||
}
|
||||
a.blue{
|
||||
display: inline-block;
|
||||
background: $blue2;
|
||||
color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 3px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
margin: 10px 0px 20px;
|
||||
&:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print
|
||||
|
||||
@media print{
|
||||
a[href]:after {
|
||||
content: none !important;
|
||||
}
|
||||
ul.steps, a.blue.pull-right, .bordered-bar span.pull-right, .left.nav-global.authenticated {
|
||||
display: none;
|
||||
}
|
||||
.shopping-cart{
|
||||
font-size: 14px;
|
||||
padding-right: 40px;
|
||||
.gray-bg{
|
||||
margin: 0;
|
||||
padding: 10px 0 20px 0;
|
||||
background: none;
|
||||
.message-left{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.bordered-bar{
|
||||
h2{
|
||||
font-size: 14px;
|
||||
}
|
||||
span{
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.user-data{
|
||||
.data-input{
|
||||
h1{
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
lms/templates/emails/business_order_confirmation_email.txt
Normal file
93
lms/templates/emails/business_order_confirmation_email.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<p>
|
||||
${_("Hi {name},").format(name=recipient_name)}
|
||||
</p>
|
||||
<p>
|
||||
${_("Thank you for your purchase of ")} <b> ${course_names} </b>
|
||||
</p>
|
||||
%if recipient_type == 'user':
|
||||
<p>${_("Your payment was successful.")}</p>
|
||||
% if marketing_link('FAQ'):
|
||||
<p>${_("If you have billing questions, please read the FAQ ({faq_url}) or contact {billing_email}.").format(billing_email=payment_support_email, faq_url=marketing_link('FAQ'))}</p>
|
||||
% else:
|
||||
<p>${_("If you have billing questions, please contact {billing_email}.").format(billing_email=payment_support_email)}</p>
|
||||
% endif
|
||||
|
||||
%elif recipient_type == 'company_contact':
|
||||
|
||||
<p>${_("{order_placed_by} placed an order and mentioned your name as the Organization contact.").format(order_placed_by=order_placed_by)}</p>
|
||||
|
||||
%elif recipient_type == 'email_recipient':
|
||||
|
||||
<p>${_("{order_placed_by} placed an order and mentioned your name as the additional receipt recipient.").format(order_placed_by=order_placed_by)}</p>
|
||||
|
||||
%endif
|
||||
|
||||
<p>${_("The items in your order are:")}</p>
|
||||
|
||||
<p>${_("Quantity - Description - Price")}<br>
|
||||
%for order_item in order_items:
|
||||
${order_item.qty} - ${order_item.line_desc} - ${"$" if order_item.currency == 'usd' else ""}${order_item.line_cost}</p>
|
||||
%endfor
|
||||
|
||||
<p>${_("Total billed to credit/debit card: {currency_symbol}{total_cost}").format(total_cost=order.total_cost, currency_symbol=("$" if order.currency == 'usd' else ""))}</p>
|
||||
|
||||
<p>
|
||||
% if order.company_name:
|
||||
${_('Company Name:')} ${order.company_name}<br>
|
||||
%endif
|
||||
% if order.customer_reference_number:
|
||||
${_('Purchase Order Number:')} ${order.customer_reference_number}<br>
|
||||
%endif
|
||||
% if order.company_contact_name:
|
||||
${_('Company Contact Name:')} ${order.company_contact_name}<br>
|
||||
%endif
|
||||
% if order.company_contact_email:
|
||||
${_('Company Contact Email:')} ${order.company_contact_email}<br>
|
||||
%endif
|
||||
% if order.recipient_name:
|
||||
${_('Recipient Name:')} ${order.recipient_name}<br>
|
||||
%endif
|
||||
% if order.recipient_email:
|
||||
${_('Recipient Email:')} ${order.recipient_email}<br>
|
||||
%endif
|
||||
|
||||
% if has_billing_info:
|
||||
${order.bill_to_cardtype} ${_("#:")} ${order.bill_to_ccnum}<br>
|
||||
${order.bill_to_first} ${order.bill_to_last}<br>
|
||||
${order.bill_to_street1}<br>
|
||||
${order.bill_to_street2}<br>
|
||||
${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode}<br>
|
||||
${order.bill_to_country.upper()}
|
||||
% endif
|
||||
</p>
|
||||
<p>${_("Order Number: {order_number}").format(order_number=order.id)}</p>
|
||||
|
||||
<p><b>${_("A CSV file of your registration URLs is attached. Please distribute registration URLs to each student planning to enroll using the email template below.")}</b></p>
|
||||
|
||||
<p>${_("Warm regards,")}<br>
|
||||
% if payment_email_signature:
|
||||
${payment_email_signature}
|
||||
% else:
|
||||
${_("The {platform_name} Team").format(platform_name=platform_name)}
|
||||
%endif
|
||||
</p>
|
||||
|
||||
|
||||
———————————————————————————————————————————
|
||||
|
||||
|
||||
<p>Dear [[Name]]</p>
|
||||
|
||||
<p>To enroll in ${course_names} we have provided a registration URL for you. Please follow the instructions below to claim your access.</p>
|
||||
|
||||
<p>Your redeem url is: [[Enter Redeem URL here from the attached CSV]]</p>
|
||||
|
||||
<p>${_("(1) Register for an account at <a href='https://{site_name}' >https://{site_name}</a>.").format(site_name=site_name)}<br>
|
||||
${_("(2) Once registered, copy the redeem URL and paste it in your web browser.")}<br>
|
||||
${_("(3) On the enrollment confirmation page, Click the 'Activate Enrollment Code' button. This will show the enrollment confirmation.")}<br>
|
||||
${_("(4) You should be able to click on 'view course' button or see your course on your student dashboard at <a href='https://{dashboard_url}'>https://{dashboard_url}</a>").format(dashboard_url=dashboard_url)}<br>
|
||||
${_("(5) Course materials will not be available until the course start date.")}</p>
|
||||
|
||||
<p>Sincerely,</p>
|
||||
<p>[[Your Signature]]</p>
|
||||
@@ -4,11 +4,17 @@ ${_("Hi {name}").format(name=order.user.profile.name)}
|
||||
${_("Your payment was successful. You will see the charge below on your next credit or debit card statement.")}
|
||||
${_("The charge will show up on your statement under the company name {merchant_name}.").format(merchant_name=settings.CC_MERCHANT_NAME)}
|
||||
% if marketing_link('FAQ'):
|
||||
${_("If you have billing questions, please read the FAQ ({faq_url}) or contact {billing_email}.").format(billing_email=settings.PAYMENT_SUPPORT_EMAIL, faq_url=marketing_link('FAQ'))}
|
||||
${_("If you have billing questions, please read the FAQ ({faq_url}) or contact {billing_email}.").format(billing_email=payment_support_email, faq_url=marketing_link('FAQ'))}
|
||||
% else:
|
||||
${_("If you have billing questions, please contact {billing_email}.").format(billing_email=settings.PAYMENT_SUPPORT_EMAIL)}
|
||||
${_("If you have billing questions, please contact {billing_email}.").format(billing_email=payment_support_email)}
|
||||
% endif
|
||||
${_("-The {platform_name} Team").format(platform_name=settings.PLATFORM_NAME)}
|
||||
|
||||
${_("Warm regards,")}
|
||||
% if payment_email_signature:
|
||||
${payment_email_signature}
|
||||
% else:
|
||||
${_("The {platform_name} Team").format(platform_name=platform_name)}
|
||||
%endif
|
||||
|
||||
${_("Your order number is: {order_number}").format(order_number=order.id)}
|
||||
|
||||
@@ -16,7 +22,7 @@ ${_("The items in your order are:")}
|
||||
|
||||
${_("Quantity - Description - Price")}
|
||||
%for order_item in order_items:
|
||||
${order_item.qty} - ${order_item.line_desc} - ${"$" if order_item.currency == 'usd' else ""}${order_item.line_cost}
|
||||
${order_item.qty} - ${order_item.line_desc} - ${"$" if order_item.currency == 'usd' else ""}${order_item.line_cost}
|
||||
%endfor
|
||||
|
||||
${_("Total billed to credit/debit card: {currency_symbol}{total_cost}").format(total_cost=order.total_cost, currency_symbol=("$" if order.currency == 'usd' else ""))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%page args="section_data"/>
|
||||
<section id="add-coupon-modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Password Reset')}">
|
||||
<section id="add-coupon-modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Add Coupon')}">
|
||||
<div class="inner-wrapper">
|
||||
<button class="close-modal">
|
||||
<i class="icon-remove"></i>
|
||||
@@ -21,7 +21,7 @@
|
||||
${_("Please enter Coupon detail below")}</p>
|
||||
</div>
|
||||
|
||||
<form id="add_coupon_form" action="${section_data['ajax_add_coupon']}" method="post" data-remote="true">
|
||||
<form id="add_coupon_form">
|
||||
<div id="coupon_form_error" class="modal-form-error"></div>
|
||||
<fieldset class="group group-form group-form-requiredinformation">
|
||||
<legend class="is-hidden">${_("Required Information")}</legend>
|
||||
@@ -54,7 +54,7 @@
|
||||
</fieldset>
|
||||
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" id="add_coupon_button" value="${_('Add Coupon')}"/>
|
||||
<input name="submit" type="button" id="add_coupon_button" value="${_('Add Coupon')}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -68,8 +68,12 @@
|
||||
<div class="wrap">
|
||||
<h2>${_("Sales")}</h2>
|
||||
<div>
|
||||
<span class="csv_tip">${_("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 e-Commerce Sales")}" data-endpoint="${ section_data['get_sale_records_url'] }" data-csv="true"></p></td>
|
||||
<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">
|
||||
</div>
|
||||
</span>
|
||||
<hr>
|
||||
<p>${_("Enter the invoice number to invalidate or re-validate sale")}</p>
|
||||
@@ -384,8 +388,28 @@
|
||||
modal_overLay.hide();
|
||||
});
|
||||
|
||||
$('#edit_coupon_form').submit(function () {
|
||||
$('#update_coupon_button').click(function () {
|
||||
$("#update_coupon_button").attr('disabled', true);
|
||||
var coupon_id = $.trim($('#coupon_id').val());
|
||||
var description = $.trim($('#edit_coupon_description').val());
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
data: {
|
||||
"coupon_id" : coupon_id,
|
||||
"description": description
|
||||
},
|
||||
url: "${section_data['ajax_update_coupon']}",
|
||||
success: function (data) {
|
||||
location.reload(true);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var data = $.parseJSON(jqXHR.responseText);
|
||||
$("#update_coupon_button").removeAttr('disabled');
|
||||
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
|
||||
$('#edit_coupon_form #coupon_form_error').text(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#course_price_link').click(function () {
|
||||
reset_input_fields();
|
||||
@@ -397,7 +421,7 @@
|
||||
reset_input_fields();
|
||||
$('input[name="generate-registration-codes-csv"]').removeAttr('disabled');
|
||||
});
|
||||
$('#set_price_form').submit(function () {
|
||||
$('#set_course_button').click(function () {
|
||||
$("#set_course_button").attr('disabled', true);
|
||||
// Get the Code and Discount value and trim it
|
||||
var course_price = $.trim($('#mode_price').val());
|
||||
@@ -422,12 +446,31 @@
|
||||
$("#set_course_button").removeAttr('disabled');
|
||||
return false;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
data: {
|
||||
"course_price" : course_price,
|
||||
"currency": currency
|
||||
},
|
||||
url: "${section_data['set_course_mode_url']}",
|
||||
success: function (data) {
|
||||
location.reload(true);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var data = $.parseJSON(jqXHR.responseText);
|
||||
$("#set_course_button").removeAttr('disabled');
|
||||
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
|
||||
$('#set_price_form #course_form_error').text(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#add_coupon_form').submit(function () {
|
||||
$('#add_coupon_button').click(function () {
|
||||
$("#add_coupon_button").attr('disabled', true);
|
||||
// Get the Code and Discount value and trim it
|
||||
var code = $.trim($('#coupon_code').val());
|
||||
var coupon_discount = $.trim($('#coupon_discount').val());
|
||||
var course_id = $.trim($('#coupon_course_id').val());
|
||||
var description = $.trim($('#coupon_description').val());
|
||||
|
||||
// Check if empty of not
|
||||
if (code === '') {
|
||||
@@ -448,36 +491,25 @@
|
||||
$('#add_coupon_form #coupon_form_error').text("${_('Please enter the numeric value for discount')}");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$('#set_price_form').on('ajax:complete', function (event, xhr) {
|
||||
if (xhr.status == 200) {
|
||||
location.reload(true);
|
||||
} else {
|
||||
$("#set_course_button").removeAttr('disabled');
|
||||
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
|
||||
$('#set_price_form #course_form_error').text(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
$('#add_coupon_form').on('ajax:complete', function (event, xhr) {
|
||||
if (xhr.status == 200) {
|
||||
location.reload(true);
|
||||
} else {
|
||||
$("#add_coupon_button").removeAttr('disabled');
|
||||
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
|
||||
$('#add_coupon_form #coupon_form_error').text(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
$('#edit_coupon_form').on('ajax:complete', function (event, xhr) {
|
||||
if (xhr.status == 200) {
|
||||
location.reload(true);
|
||||
} else {
|
||||
$("#update_coupon_button").removeAttr('disabled');
|
||||
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
|
||||
$('#edit_coupon_form #coupon_form_error').text(xhr.responseText);
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
data: {
|
||||
"code" : code,
|
||||
"discount": coupon_discount,
|
||||
"course_id": course_id,
|
||||
"description": description
|
||||
},
|
||||
url: "${section_data['ajax_add_coupon']}",
|
||||
success: function (data) {
|
||||
location.reload(true);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var data = $.parseJSON(jqXHR.responseText);
|
||||
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
|
||||
$('#add_coupon_form #coupon_form_error').text(data.message);
|
||||
$("#add_coupon_button").removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
// removing close link's default behavior
|
||||
$('.close-modal').click(function (e) {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<div class="submit">
|
||||
<input type="hidden" name="coupon_id" id="coupon_id"/>
|
||||
<input name="submit" type="submit" id="update_coupon_button" value="${_('Update Coupon')}"/>
|
||||
<input name="submit" type="button" id="update_coupon_button" value="${_('Update Coupon')}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
${_("Please enter Course Mode detail below")}</p>
|
||||
</div>
|
||||
|
||||
<form id="set_price_form" action="${section_data['set_course_mode_url']}" method="post" data-remote="true">
|
||||
<form id="set_price_form">
|
||||
<div id="course_form_error" class="modal-form-error"></div>
|
||||
<fieldset class="group group-form group-form-requiredinformation">
|
||||
<legend class="is-hidden">${_("Required Information")}</legend>
|
||||
@@ -40,7 +40,7 @@
|
||||
</ol>
|
||||
</fieldset>
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" id="set_course_button" value="${_('Set Price')}"/>
|
||||
<input name="submit" type="button" id="set_course_button" value="${_('Set Price')}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
114
lms/templates/shoppingcart/billing_details.html
Normal file
114
lms/templates/shoppingcart/billing_details.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<%inherit file="shopping_cart_flow.html" />
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<%block name="billing_details_highlight"><li class="active" >${_('Billing Details')}</li></%block>
|
||||
<%block name="confirmation_highlight"></%block>
|
||||
|
||||
<%block name="custom_content">
|
||||
<div class="container">
|
||||
% if shoppingcart_items:
|
||||
<section class="confirm-enrollment shopping-cart">
|
||||
<h3>${_('You can proceed to payment at any point in time. Any additional information you provide will be included in your receipt.')}</h3>
|
||||
<div class="billing-data">
|
||||
<div class="col-half">
|
||||
<h3>${_('Purchasing Organizational Details')}</h3>
|
||||
<div class="data-group"><label for="id_company_name">${_('Purchasing organization')}</label><input type="text" id="id_company_name" name="company_name"></div>
|
||||
<div class="data-group"><label for="id_customer_reference_number">${_('Purchase order number (if any)')}</label><input type="text" id="id_customer_reference_number" maxlength="63" name="customer_reference_number"></div>
|
||||
</div>
|
||||
<div class="col-half">
|
||||
<h3>${_('Organization Contact')}</h3>
|
||||
<div class="data-group"><label for="id_company_contact_name">${_('Name')}</label><input type="text"id="id_company_contact_name" name="company_contact_name"></div>
|
||||
<div class="data-group"><label for="id_company_contact_email">${_('Email Address')}</label><input type="email" placeholder="${_('email@example.com')}" id="id_company_contact_email" name="company_contact_email"><span id="company_contact_email_error" class="error-text"></span></div>
|
||||
</div>
|
||||
<div class="col-half">
|
||||
<h3>${_('Additional Receipt Recipient')}</h3>
|
||||
<div class="data-group">
|
||||
<label for="id_recipient_name">${_('Name')}</label>
|
||||
<input type="text" id="id_recipient_name" name="recipient_name">
|
||||
</div>
|
||||
<div class="data-group">
|
||||
<label for="id_recipient_email">${_('Email Address')}</label>
|
||||
<input type="email" id="id_recipient_email" placeholder="${_('email@example.com')}" name="recipient_email">
|
||||
<span id="recipient_email_error" class="error-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="discount">
|
||||
<div class="code-text">
|
||||
<span class="pull-right">${_('Total')}: <b>$${"{0:0.2f}".format(amount)} USD</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-two">
|
||||
|
||||
<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')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disclaimer">${_('Payment processing occurs on a separate secure site.')}</div>
|
||||
|
||||
</section>
|
||||
%else:
|
||||
<div class="empty-cart" >
|
||||
<h2>${_('Your Shopping cart is currently empty.')}</h2>
|
||||
<a href="${marketing_link('COURSES')}" class="blue">${_('View Courses')}</a>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
function validateEmail(sEmail) {
|
||||
filter = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/
|
||||
return filter.test(sEmail)
|
||||
}
|
||||
$('form input[type="submit"]').click(function(event) {
|
||||
var is_valid_email = true;
|
||||
var payment_form = $(this).parent('form');
|
||||
var recipient_email = $('input[name="recipient_email"]').val();
|
||||
var company_contact_email = $('input[name="company_contact_email"]').val();
|
||||
if ( recipient_email != '' && !(validateEmail(recipient_email))) {
|
||||
$('span#recipient_email_error').html('Please enter valid email address');
|
||||
$('input[name="recipient_email"]').addClass('error');
|
||||
is_valid_email = false;
|
||||
}
|
||||
else {
|
||||
$('input[name="recipient_email"]').removeClass('error');
|
||||
$('span#recipient_email_error').html('');
|
||||
}
|
||||
if ( company_contact_email != '' && !(validateEmail(company_contact_email))) {
|
||||
$('span#company_contact_email_error').html('Please enter valid email address');
|
||||
$('input[name="company_contact_email"]').addClass('error');
|
||||
is_valid_email = false;
|
||||
}
|
||||
else {
|
||||
$('input[name="company_contact_email"]').removeClass('error');
|
||||
$('span#company_contact_email_error').html('');
|
||||
}
|
||||
if (!is_valid_email) {
|
||||
return false;
|
||||
}
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('billing_details')}";
|
||||
var data = {
|
||||
"company_name" : $('input[name="company_name"]').val(),
|
||||
"company_contact_name" : $('input[name="company_contact_name"]').val(),
|
||||
"company_contact_email" : company_contact_email,
|
||||
"recipient_name" : $('input[name="recipient_name"]').val(),
|
||||
"recipient_email" : recipient_email,
|
||||
"customer_reference_number" : $('input[name="customer_reference_number"]').val()
|
||||
};
|
||||
$.post(post_url, data)
|
||||
.success(function(data) {
|
||||
payment_form.submit();
|
||||
})
|
||||
.error(function(data,status) {
|
||||
|
||||
})
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -3,5 +3,5 @@
|
||||
<input type="hidden" name="${pk}" value="${pv}" />
|
||||
% endfor
|
||||
|
||||
<input type="submit" value="Check Out" />
|
||||
<i class="icon-caret-right"></i><input type="submit" value="Payment"/>
|
||||
</form>
|
||||
@@ -1,129 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="pagetitle">${_("Your Shopping Cart")}</%block>
|
||||
|
||||
<section class="container cart-list">
|
||||
<h2>${_("Your selected items:")}</h2>
|
||||
<h3 class="cart-errors" id="cart-error">Error goes here.</h3>
|
||||
% if shoppingcart_items:
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr class="cart-headings">
|
||||
<th class="dsc">${_("Description")}</th>
|
||||
<th class="u-pr">${_("Price")}</th>
|
||||
<th class="cur">${_("Currency")}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for item in shoppingcart_items:
|
||||
<tr class="cart-items">
|
||||
<td>${item.line_desc}</td>
|
||||
<td>
|
||||
${"{0:0.2f}".format(item.unit_cost)}
|
||||
% if item.list_price != None:
|
||||
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
|
||||
% endif
|
||||
</td>
|
||||
<td>${item.currency.upper()}</td>
|
||||
<td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td>
|
||||
</tr>
|
||||
% endfor
|
||||
<tr class="always-gray">
|
||||
<td colspan="4" valign="middle" class="cart-total" align="right">
|
||||
<b>${_("Total Amount")}: <span> ${"{0:0.2f}".format(amount)} </span> </b>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="always-white">
|
||||
<td colspan="2">
|
||||
<input type="text" placeholder="Enter code here" name="cart_code" id="code">
|
||||
<input type="button" value="Apply Code" id="cart-code">
|
||||
</td>
|
||||
<td colspan="4" align="right">
|
||||
% if amount == 0:
|
||||
<input type="button" value = "Register" id="register" >
|
||||
% else:
|
||||
${form_html}
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tfoot>
|
||||
</table>
|
||||
<!-- <input id="back_input" type="submit" value="Return" /> -->
|
||||
% else:
|
||||
<p>${_("You have selected no items for purchase.")}</p>
|
||||
% endif
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('a.remove_line_item').click(function(event) {
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.remove_item')}";
|
||||
$.post(post_url, {id:$(this).data('item-id')})
|
||||
.always(function(data){
|
||||
location.reload(true);
|
||||
});
|
||||
});
|
||||
|
||||
$('#cart-code').click(function(event){
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.use_code')}";
|
||||
$.post(post_url,{
|
||||
"code" : $('#code').val(),
|
||||
beforeSend: function(xhr, options){
|
||||
if($('#code').val() == "") {
|
||||
showErrorMsgs('Must enter a valid code')
|
||||
xhr.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.success(function(data) {
|
||||
location.reload(true);
|
||||
})
|
||||
.error(function(data,status) {
|
||||
if(status=="parsererror"){
|
||||
location.reload(true);
|
||||
}else{
|
||||
showErrorMsgs(data.responseText)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#register').click(function(event){
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.register_courses')}";
|
||||
$.post(post_url)
|
||||
.success(function(data) {
|
||||
window.location.href = "${reverse('dashboard')}";
|
||||
})
|
||||
.error(function(data,status) {
|
||||
if(status=="parsererror"){
|
||||
location.reload(true);
|
||||
}else{
|
||||
showErrorMsgs(data.responseText)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#back_input').click(function(){
|
||||
history.back();
|
||||
});
|
||||
|
||||
function showErrorMsgs(msg){
|
||||
$(".cart-errors").css('display', 'block');
|
||||
$("#cart-error").html(msg);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,108 +1,356 @@
|
||||
<%inherit file="shopping_cart_flow.html" />
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from django.conf import settings %>
|
||||
<%! from microsite_configuration import microsite %>
|
||||
<%!
|
||||
from courseware.courses import course_image_url, get_course_about_section, get_course_by_id
|
||||
%>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
<%block name="billing_details_highlight">
|
||||
% if order_type == 'business':
|
||||
<li>${_('Billing Details')}</li>
|
||||
%endif
|
||||
</%block>
|
||||
|
||||
<%block name="bodyclass">purchase-receipt</%block>
|
||||
|
||||
<%block name="pagetitle">${_("Register for [Course Name] | Receipt (Order")} ${order.id})</%block>
|
||||
|
||||
<%block name="content">
|
||||
<%block name="confirmation_highlight">class="active"</%block>
|
||||
|
||||
<%block name="custom_content">
|
||||
<div class="container">
|
||||
<section class="notification">
|
||||
<h2>${_("Thank you for your Purchase!")}</h2>
|
||||
<p>${_("Please print this receipt page for your records. You should also have received a receipt in your email.")}</p>
|
||||
% for inst in instructions:
|
||||
<p>${inst}</p>
|
||||
% endfor
|
||||
% 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 cart-list">
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
<h1>${_("{platform_name} ({site_name}) Electronic Receipt").format(platform_name=microsite.get_value('platform_name', settings.PLATFORM_NAME), site_name=microsite.get_value('SITE_NAME', settings.SITE_NAME))}</h1>
|
||||
<hr />
|
||||
|
||||
<table class="order-receipt">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><h3 class="order-number">${_("Order #")}${order.id}</h3></td>
|
||||
<td></td>
|
||||
<td colspan="2"><h3 class="order-date">${_("Date:")} ${order.purchase_time.date().isoformat()}</h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5"><h2 class="items-ordered">${_("Items ordered:")}</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="qty">${_("Qty")}</th>
|
||||
<th class="desc">${_("Description")}</th>
|
||||
<th class="url">${_("URL")}</th>
|
||||
<th class="u-pr">${_("Unit Price")}</th>
|
||||
<th class="pri">${_("Price")}</th>
|
||||
<th class="curr">${_("Currency")}</th>
|
||||
</tr>
|
||||
% for item in order_items:
|
||||
|
||||
|
||||
<tr class="order-item">
|
||||
% if item.status == "purchased":
|
||||
<td>${item.qty}</td>
|
||||
<td>${item.line_desc}</td>
|
||||
<td>
|
||||
% if item.course_id:
|
||||
<% course_id = reverse('info', args=[item.course_id.to_deprecated_string()]) %>
|
||||
<a href="${course_id | h}" class="enter-course">${_('View Course')}</a></td>
|
||||
% endif
|
||||
</td>
|
||||
<td>${"{0:0.2f}".format(item.unit_cost)}
|
||||
% if item.list_price != None:
|
||||
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
|
||||
% endif
|
||||
</td>
|
||||
<td>${"{0:0.2f}".format(item.line_cost)}</td>
|
||||
<td>${item.currency.upper()}</td></tr>
|
||||
% elif item.status == "refunded":
|
||||
<td><del>${item.qty}</del></td>
|
||||
<td><del>${item.line_desc}</del></td>
|
||||
<td><del>${"{0:0.2f}".format(item.unit_cost)}</del></td>
|
||||
<td><del>${"{0:0.2f}".format(item.line_cost)}</del></td>
|
||||
<td><del>${item.currency.upper()}</del></td></tr>
|
||||
% endif
|
||||
% endfor
|
||||
<tr>
|
||||
<td colspan="3"></td>
|
||||
<th>${_("Total Amount")}</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"></td>
|
||||
<td>${"{0:0.2f}".format(order.total_cost)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
% if any_refunds:
|
||||
<p>
|
||||
## Translators: Please keep the "<del>" and "</del>" tags around your translation of the word "this" in your translation.
|
||||
${_("Note: items with strikethough like <del>this</del> have been refunded.")}
|
||||
</p>
|
||||
<section class="wrapper confirm-enrollment shopping-cart print">
|
||||
<div class="gray-bg">
|
||||
<div class="message-left">
|
||||
<% courses_url = reverse('courses') %>
|
||||
% if order_type == 'personal':
|
||||
## in case of multiple courses in single self purchase scenario,
|
||||
## we will show the button View Dashboard
|
||||
% if len(shoppingcart_items) > 1 :
|
||||
<% dashboard_url = reverse('dashboard') %>
|
||||
<a href="${dashboard_url}" class="blue pull-right">${_("View Dashboard")} <i class="icon-caret-right"></i></a>
|
||||
% elif shoppingcart_items and shoppingcart_items[0][1]:
|
||||
<% course = shoppingcart_items[0][1] %>
|
||||
<% course_info_url = reverse('info', kwargs={'course_id': course.id.to_deprecated_string()}) %>
|
||||
<a href="${course_info_url}" class="blue pull-right">${_("View Course")} <i class="icon-caret-right"></i></a>
|
||||
%endif
|
||||
${_("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)}
|
||||
% elif order_type == 'business':
|
||||
% if total_registration_codes > 1 :
|
||||
<% code_plural_form = 'codes' %>
|
||||
% else:
|
||||
<% code_plural_form = 'code' %>
|
||||
% endif
|
||||
${_("You have successfully purchased <b>{total_registration_codes} course registration codes</b> "
|
||||
"for <b>{appended_course_names}. </b>"
|
||||
"The following receipt has been emailed to <strong>{appended_recipient_emails}</strong>"
|
||||
).format(total_registration_codes=total_registration_codes, appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)}
|
||||
% endif
|
||||
% if order.total_cost > 0:
|
||||
<h2>${_("Billed To:")}</h2>
|
||||
<p>
|
||||
${order.bill_to_cardtype} ${_("#:")} ${order.bill_to_ccnum}<br />
|
||||
${order.bill_to_first} ${order.bill_to_last}<br />
|
||||
${order.bill_to_street1}<br />
|
||||
${order.bill_to_street2}<br />
|
||||
${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode}<br />
|
||||
${order.bill_to_country.upper()}<br />
|
||||
</p>
|
||||
% endif
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
% if order_type == 'business':
|
||||
<h3 class="text-center">${_("Please send each professional one of these unique registration codes to enroll into the course. The confirmation/receipt email you will receive has an example email template with directions for the individuals enrolling.")}.</h3>
|
||||
<table class="course-receipt">
|
||||
<thead>
|
||||
<th>${_("Course Name")}</th>
|
||||
<th>${_("Enrollment Code")}</th>
|
||||
<th>${_("Enrollment Link")}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for registration_code in registration_codes:
|
||||
<% course = get_course_by_id(registration_code.course_id, depth=0) %>
|
||||
<tr>
|
||||
<td>${_("{course_name}").format(course_name=course.display_name)}</td>
|
||||
<td>${registration_code.code}</td>
|
||||
|
||||
<% redemption_url = reverse('register_code_redemption', args = [registration_code.code] ) %>
|
||||
<% enrollment_url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url) %>
|
||||
<td><a href="${redemption_url}">${enrollment_url}</a></td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
%endif
|
||||
<div class="bordered-bar">
|
||||
<h2>${_('Invoice')} #${order.id}<span>${_('Date of purchase')}: ${order_purchase_date} </span><span
|
||||
class="pull-right"><a href="" onclick="window.print();" class="blue-link"><i class="icon-print"></i> ${_('Print Receipt')}</a></span>
|
||||
</h2>
|
||||
</div>
|
||||
% if order.total_cost > 0:
|
||||
<div class="pattern">
|
||||
<h2> ${_("Billed To Details")}: </h2>
|
||||
|
||||
<div class="col-two no-border">
|
||||
% if order_type == 'business':
|
||||
<div class="row">
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Company Name')}:</b>
|
||||
<label>
|
||||
% if order.company_name:
|
||||
${_("{company_name}").format(company_name=order.company_name)}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Purchase Order Number')}:</b>
|
||||
<label>
|
||||
% if order.customer_reference_number:
|
||||
${_("{customer_reference_number}").format(customer_reference_number=order.customer_reference_number)}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Company Contact Name')}:</b>
|
||||
<label>
|
||||
% if order.company_contact_name:
|
||||
${_("{company_contact_name}").format(company_contact_name=order.company_contact_name)}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Company Contact Email')}:</b>
|
||||
<label>
|
||||
% if order.company_contact_email:
|
||||
${ order.company_contact_email }
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Recipient Name')}:</b>
|
||||
<label>
|
||||
% if order.recipient_name:
|
||||
${_("{recipient_name}").format(recipient_name=order.recipient_name)}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Recipient Email')}:</b>
|
||||
<label>
|
||||
% if order.recipient_email:
|
||||
${order.recipient_email}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
<div class="row">
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Card Type')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_cardtype:
|
||||
${order.bill_to_cardtype}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Credit Card Number')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_ccnum:
|
||||
${order.bill_to_ccnum}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Name')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_first or order.bill_to_last:
|
||||
${order.bill_to_first} ${order.bill_to_last}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Address 1')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_street1:
|
||||
${order.bill_to_street1}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Address 2')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_street2:
|
||||
${order.bill_to_street2}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('City')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_city:
|
||||
${order.bill_to_city}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('State')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_state:
|
||||
${order.bill_to_state}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-inside">
|
||||
<p>
|
||||
<b>${_('Country')}:</b>
|
||||
<label>
|
||||
% if order.bill_to_country:
|
||||
${order.bill_to_country.upper()}
|
||||
% else:
|
||||
N/A
|
||||
% endif
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<hr class="border"/>
|
||||
% for item, course in shoppingcart_items:
|
||||
% if loop.index > 0 :
|
||||
<hr>
|
||||
%endif
|
||||
<div class="user-data">
|
||||
<div class="clearfix">
|
||||
<div class="image">
|
||||
<img style="width: 100%; height: 100%;" src="${course_image_url(course)}"
|
||||
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Image"/>
|
||||
</div>
|
||||
<div class="data-input">
|
||||
<h3>${_("Registration for")}:
|
||||
<span class="pull-right">
|
||||
% if course.start_date_text or course.end_date_text:
|
||||
${_("Course Dates")}:
|
||||
%endif
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<h1>${_(" {course_name} ").format(course_name=course.display_name)}
|
||||
<span class="pull-right">
|
||||
% if course.start_date_text:
|
||||
${course.start_date_text}
|
||||
%endif
|
||||
-
|
||||
% if course.end_date_text:
|
||||
${course.end_date_text}
|
||||
%endif
|
||||
</span>
|
||||
</h1>
|
||||
<hr/>
|
||||
<div class="three-col">
|
||||
% if item.status == "purchased":
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
<div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span>
|
||||
</div>
|
||||
<div class="price green">${_('Discount Applied:')} <span> $${"{0:0.2f}".format(item.unit_cost)} </span></div>
|
||||
% else:
|
||||
<div class="price">${_('Price per student:')} <span> $${"{0:0.2f}".format(item.unit_cost)}</span></div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="numbers-row">
|
||||
<label>${_("Students")}:</label>
|
||||
<div class="counter no-border text-dark-grey">
|
||||
${item.qty}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif item.status == "refunded":
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
<div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span>
|
||||
</div>
|
||||
<div class="price green">${_('Discount Applied:')} <span><del> $${"{0:0.2f}".format(item.unit_cost)}
|
||||
</del></span></div>
|
||||
% else:
|
||||
<div class="price">${_('Price per student:')} <span><del> $${"{0:0.2f}".format(item.unit_cost)}</del></span>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="numbers-row">
|
||||
<label>${_("Students")}:</label>
|
||||
<div class="counter no-border">
|
||||
<del>${item.qty}</del>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
<div class="discount">
|
||||
<div class="code-text">
|
||||
% if any_refunds:
|
||||
<span>
|
||||
## Translators: Please keep the "<del>" and "</del>" tags around your translation of the word "this" in your translation.
|
||||
${_("Note: items with strikethough like <del>this</del> have been refunded.")}
|
||||
</span>
|
||||
% endif
|
||||
<span class="pull-right">${_("Total")}: <b>$${"{0:0.2f}".format(order.total_cost)} USD</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
288
lms/templates/shoppingcart/shopping_cart.html
Normal file
288
lms/templates/shoppingcart/shopping_cart.html
Normal file
@@ -0,0 +1,288 @@
|
||||
<%inherit file="shopping_cart_flow.html" />
|
||||
<%block name="review_highlight">class="active"</%block>
|
||||
|
||||
<%!
|
||||
from courseware.courses import course_image_url, get_course_about_section
|
||||
from django.core.urlresolvers import reverse
|
||||
from edxmako.shortcuts import marketing_link
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
%>
|
||||
|
||||
<%block name="custom_content">
|
||||
|
||||
<div class="container">
|
||||
% if shoppingcart_items:
|
||||
<%block name="billing_details_highlight">
|
||||
% if order.order_type == 'business':
|
||||
<li>${_('Billing Details')}</li>
|
||||
% endif
|
||||
</%block>
|
||||
<% discount_applied = False %>
|
||||
<section class="wrapper confirm-enrollment shopping-cart">
|
||||
% for item, course in shoppingcart_items:
|
||||
% if loop.index > 0 :
|
||||
<hr>
|
||||
%endif
|
||||
<div class="user-data">
|
||||
<div class="clearfix">
|
||||
<div class="image">
|
||||
|
||||
<img style="width: 100%; height: 100%;" src="${course_image_url(course)}"
|
||||
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Cover Image" />
|
||||
|
||||
</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_date_text} - ${course.end_date_text}</span></h1>
|
||||
<hr />
|
||||
<div class="three-col">
|
||||
<div class="col-1">
|
||||
% if item.list_price != None:
|
||||
<% discount_applied = True %>
|
||||
<div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span></div>
|
||||
<div class="price green">${_('Discount Applied:')} <span> $${"{0:0.2f}".format(item.unit_cost)} </span></div>
|
||||
% else:
|
||||
<div class="price">${_('Price per student:')} <span> $${"{0:0.2f}".format(item.unit_cost)}</span></div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<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}" >
|
||||
</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>
|
||||
<span class="error-text hidden" id="students-${item.id}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<a href="#" class="btn-remove" data-item-id="${item.id}"><i class="icon-remove-sign"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endfor
|
||||
<div class="discount">
|
||||
|
||||
<div class="code-text">
|
||||
% if not discount_applied:
|
||||
<div class="code-input">
|
||||
<input type="text" placeholder="discount or activation code" id="input_code">
|
||||
<input type="submit" value="Apply" class="blue" id="submit-code">
|
||||
<span class="error-text hidden" id="code" ></span>
|
||||
</div>
|
||||
% else:
|
||||
<div class="code-applied">
|
||||
<span class="green"><i class="icon-ok"></i>${_('code has been applied')}</span>
|
||||
<input type="submit" value="Reset" class="blue-border" id="submit-reset-redemption">
|
||||
</div>
|
||||
%endif
|
||||
<span class="pull-right">${_('Total:')} <b id="total-amount">$${"{0:0.2f}".format(amount)} USD</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:
|
||||
${form_html}
|
||||
<p>
|
||||
${_('After this purchase is complete,')}<br/><b>${order.user.username}</b>
|
||||
${_('will be enrolled in this course.')}
|
||||
</p>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
% else:
|
||||
<div class="empty-cart" >
|
||||
<h2>${_('Your Shopping cart is currently empty.')}</h2>
|
||||
<a href="${marketing_link('COURSES')}" class="blue">${_('View Courses')}</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
</%block>
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
$('a.btn-remove').click(function(event) {
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.remove_item')}";
|
||||
$.post(post_url, {id:$(this).data('item-id')})
|
||||
.always(function(data){
|
||||
location.reload(true);
|
||||
});
|
||||
});
|
||||
|
||||
$('#submit-code').click(function(event){
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.use_code')}";
|
||||
if($('#input_code').val() == "") {
|
||||
showErrorMsgs('Must enter a valid code','code');
|
||||
return;
|
||||
}
|
||||
$.post(post_url,{
|
||||
"code" : $('#input_code').val()
|
||||
}
|
||||
)
|
||||
.success(function(data) {
|
||||
location.reload(true);
|
||||
})
|
||||
.error(function(data,status) {
|
||||
if(status=="parsererror"){
|
||||
location.reload(true);
|
||||
}else{
|
||||
showErrorMsgs(data.responseText, 'code')
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#submit-reset-redemption').click(function(event){
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.reset_code_redemption')}";
|
||||
$.post(post_url)
|
||||
.success(function(data) {
|
||||
location.reload(true);
|
||||
})
|
||||
.error(function(data,status) {
|
||||
if(status=="parsererror"){
|
||||
location.reload(true);
|
||||
}else{
|
||||
showErrorMsgs(data.responseText,'code')
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#register').click(function(event){
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.register_courses')}";
|
||||
$.post(post_url)
|
||||
.success(function(data) {
|
||||
window.location.href = "${reverse('dashboard')}";
|
||||
})
|
||||
.error(function(data,status) {
|
||||
if(status=="parsererror"){
|
||||
location.reload(true);
|
||||
}else{
|
||||
showErrorMsgs(data.responseText)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Don't allow decrementing below one
|
||||
if (oldValue > 1) {
|
||||
newVal = parseFloat(oldValue) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
$button.parent().find("input").val(newVal);
|
||||
$('#updateBtn-'+ItemId).removeClass('hidden');
|
||||
|
||||
});
|
||||
|
||||
$('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 showErrorMsgs(msg, msg_area){
|
||||
|
||||
$( "span.error-text#"+ msg_area +"" ).removeClass("hidden");
|
||||
$( "span.error-text#"+ msg_area +"" ).html(msg).show();
|
||||
|
||||
if(msg_area=='code'){
|
||||
$("#input_code").addClass('error');
|
||||
}
|
||||
}
|
||||
|
||||
function hideErrorMsg(msg_area){
|
||||
$( "span.error-text#"+ msg_area +"" ).addClass("hidden");
|
||||
}
|
||||
|
||||
function update_user_cart(ItemId, number_of_students){
|
||||
var post_url = "${reverse('shoppingcart.views.update_user_cart')}";
|
||||
$.post(post_url, {
|
||||
ItemId:ItemId,
|
||||
qty:number_of_students
|
||||
}
|
||||
)
|
||||
.success(function(data) {
|
||||
location.reload(true);
|
||||
})
|
||||
.error(function(data,status) {
|
||||
location.reload(true);
|
||||
})
|
||||
}
|
||||
|
||||
$('input[name="students"]').on("click", function() {
|
||||
$('#updateBtn-'+this.id).removeClass('hidden');
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
</script>
|
||||
27
lms/templates/shoppingcart/shopping_cart_flow.html
Normal file
27
lms/templates/shoppingcart/shopping_cart_flow.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%inherit file="../main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%block name="pagetitle">${_("Shopping cart")}</%block>
|
||||
|
||||
|
||||
|
||||
<%block name="bodyextra">
|
||||
|
||||
<div class="container">
|
||||
<section class="wrapper confirm-enrollment shopping-cart">
|
||||
<h1> ${_("{site_name} - Shopping Cart").format(site_name=site_name)}</h1>
|
||||
% if shoppingcart_items:
|
||||
<ul class="steps">
|
||||
<li <%block name="review_highlight"/>>${_('Review')}</li>
|
||||
<%block name="billing_details_highlight"/>
|
||||
<li <%block name="payment_highlight"/>>${_('Payment')}</li>
|
||||
<li <%block name="confirmation_highlight"/>>${_('Confirmation')}</li>
|
||||
</ul>
|
||||
%endif
|
||||
</section>
|
||||
</div>
|
||||
<%block name="custom_content"/>
|
||||
|
||||
</%block>
|
||||
@@ -103,7 +103,7 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
% for item in order_items:
|
||||
% for item, course in shoppingcart_items:
|
||||
<tr>
|
||||
<td>${item.line_desc}</td>
|
||||
<td>
|
||||
@@ -158,7 +158,7 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
% for item in order_items:
|
||||
% for item, course in shoppingcart_items:
|
||||
<tr>
|
||||
% if item.status == "purchased":
|
||||
<td>${order.id}</td>
|
||||
|
||||
Reference in New Issue
Block a user