Commit 1abf064d by chrisndodge

Merge pull request #5235 from edx/cdodge/shopping-cart-rewrite

Cdodge/shopping cart rewrite
parents be19c1a9 4f7c4949
...@@ -446,6 +446,10 @@ def is_course_blocked(request, redeemed_registration_codes, course_key): ...@@ -446,6 +446,10 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
"""Checking either registration is blocked or not .""" """Checking either registration is blocked or not ."""
blocked = False blocked = False
for redeemed_registration in redeemed_registration_codes: for redeemed_registration in redeemed_registration_codes:
# 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'): if not getattr(redeemed_registration.invoice, 'is_valid'):
blocked = True blocked = True
# disabling email notifications for unpaid registration courses # disabling email notifications for unpaid registration courses
......
...@@ -39,7 +39,7 @@ from course_modes.models import CourseMode ...@@ -39,7 +39,7 @@ from course_modes.models import CourseMode
from open_ended_grading import open_ended_notifications from open_ended_grading import open_ended_notifications
from student.models import UserTestGroup, CourseEnrollment 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 util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -277,11 +277,21 @@ def index(request, course_id, chapter=None, section=None, ...@@ -277,11 +277,21 @@ def index(request, course_id, chapter=None, section=None,
user = User.objects.prefetch_related("groups").get(id=request.user.id) 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(
redeemed_registration_codes = CourseRegistrationCode.objects.filter(course_id=course_key, registrationcoderedemption__redeemed_by=request.user) course_id=course_key,
for redeemed_registration in redeemed_registration_codes: registrationcoderedemption__redeemed_by=request.user
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())
# 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')) return redirect(reverse('dashboard'))
request.user = user # keep just one instance of User request.user = user # keep just one instance of User
...@@ -703,7 +713,8 @@ def course_about(request, course_id): ...@@ -703,7 +713,8 @@ def course_about(request, course_id):
settings.PAID_COURSE_REGISTRATION_CURRENCY[0]) settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
if request.user.is_authenticated(): if request.user.is_authenticated():
cart = shoppingcart.models.Order.get_cart_for_user(request.user) 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_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()) reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string())
......
...@@ -1448,6 +1448,21 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1448,6 +1448,21 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
response = self.client.get(url + '/csv', {}) response = self.client.get(url + '/csv', {})
self.assertEqual(response['Content-Type'], 'text/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): def test_get_sale_records_features_csv(self):
""" """
Test that the response from get_sale_records is in csv format. 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 ...@@ -588,7 +588,47 @@ def get_sale_records(request, course_id, csv=False): # pylint: disable=W0613, W
return JsonResponse(response_payload) return JsonResponse(response_payload)
else: else:
header, datarows = instructor_analytics.csvs.format_dictlist(sale_data, query_features) 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') @require_level('staff')
...@@ -766,7 +806,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=W0613 ...@@ -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) 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 recursive function that generate a new code every time and saves in the Course Registration Table
if validation check passes if validation check passes
...@@ -776,16 +816,16 @@ def save_registration_codes(request, course_id, generated_codes_list, invoice): ...@@ -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 # check if the generated code is in the Coupon Table
matching_coupons = Coupon.objects.filter(code=code, is_active=True) matching_coupons = Coupon.objects.filter(code=code, is_active=True)
if matching_coupons: 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( 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: try:
course_registration.save() course_registration.save()
generated_codes_list.append(course_registration) return course_registration
except IntegrityError: 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): def registration_codes_csv(file_name, codes_list, csv_type=None):
...@@ -851,7 +891,6 @@ def generate_registration_codes(request, course_id): ...@@ -851,7 +891,6 @@ def generate_registration_codes(request, course_id):
Respond with csv which contains a summary of all Generated Codes. Respond with csv which contains a summary of all Generated Codes.
""" """
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_registration_codes = []
invoice_copy = False invoice_copy = False
# covert the course registration code number into integer # covert the course registration code number into integer
...@@ -888,8 +927,10 @@ def generate_registration_codes(request, course_id): ...@@ -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, 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 internal_reference=internal_reference, customer_reference_number=customer_reference_number
) )
registration_codes = []
for _ in range(course_code_number): # pylint: disable=W0621 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') site_name = microsite.get_value('SITE_NAME', 'localhost')
course = get_course_by_id(course_id, depth=None) course = get_course_by_id(course_id, depth=None)
...@@ -916,7 +957,7 @@ def generate_registration_codes(request, course_id): ...@@ -916,7 +957,7 @@ def generate_registration_codes(request, course_id):
'discount': discount, 'discount': discount,
'sale_price': sale_price, 'sale_price': sale_price,
'quantity': quantity, 'quantity': quantity,
'registration_codes': course_registration_codes, 'registration_codes': registration_codes,
'course_url': course_url, 'course_url': course_url,
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME), 'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
'dashboard_url': dashboard_url, 'dashboard_url': dashboard_url,
...@@ -934,7 +975,7 @@ def generate_registration_codes(request, course_id): ...@@ -934,7 +975,7 @@ def generate_registration_codes(request, course_id):
#send_mail(subject, message, from_address, recipient_list, fail_silently=False) #send_mail(subject, message, from_address, recipient_list, fail_silently=False)
csv_file = StringIO.StringIO() csv_file = StringIO.StringIO()
csv_writer = csv.writer(csv_file) 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]) csv_writer.writerow([registration_code.code])
# send a unique email for each recipient, don't put all email addresses in a single email # 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): ...@@ -948,7 +989,7 @@ def generate_registration_codes(request, course_id):
email.attach(u'Invoice.txt', invoice_attachment, 'text/plain') email.attach(u'Invoice.txt', invoice_attachment, 'text/plain')
email.send() email.send()
return registration_codes_csv("Registration_Codes.csv", course_registration_codes) return registration_codes_csv("Registration_Codes.csv", registration_codes)
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -23,6 +23,8 @@ urlpatterns = patterns('', # nopep8 ...@@ -23,6 +23,8 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"), 'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"),
url(r'^get_sale_records(?P<csv>/csv)?$', url(r'^get_sale_records(?P<csv>/csv)?$',
'instructor.views.api.get_sale_records', name="get_sale_records"), '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$', url(r'^sale_validation_url$',
'instructor.views.api.sale_validation', name="sale_validation"), 'instructor.views.api.sale_validation', name="sale_validation"),
url(r'^get_anon_ids$', url(r'^get_anon_ids$',
......
...@@ -62,27 +62,36 @@ def add_coupon(request, course_id): # pylint: disable=W0613 ...@@ -62,27 +62,36 @@ def add_coupon(request, course_id): # pylint: disable=W0613
# check if the coupon code is in the CourseRegistrationCode Table # check if the coupon code is in the CourseRegistrationCode Table
course_registration_code = CourseRegistrationCode.objects.filter(code=code) course_registration_code = CourseRegistrationCode.objects.filter(code=code)
if course_registration_code: if course_registration_code:
return HttpResponseNotFound(_( return JsonResponse(
"The code ({code}) that you have tried to define is already in use as a registration code").format(code=code) {'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') description = request.POST.get('description')
course_id = request.POST.get('course_id') course_id = request.POST.get('course_id')
try: try:
discount = int(request.POST.get('discount')) discount = int(request.POST.get('discount'))
except ValueError: 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: 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( coupon = Coupon(
code=code, description=description, course_id=course_id, code=code, description=description, course_id=course_id,
percentage_discount=discount, created_by_id=request.user.id percentage_discount=discount, created_by_id=request.user.id
) )
coupon.save() 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: 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 @require_POST
...@@ -93,17 +102,21 @@ def update_coupon(request, course_id): # pylint: disable=W0613 ...@@ -93,17 +102,21 @@ def update_coupon(request, course_id): # pylint: disable=W0613
""" """
coupon_id = request.POST.get('coupon_id', None) coupon_id = request.POST.get('coupon_id', None)
if not coupon_id: 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: try:
coupon = Coupon.objects.get(pk=coupon_id) coupon = Coupon.objects.get(pk=coupon_id)
except ObjectDoesNotExist: 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') description = request.POST.get('description')
coupon.description = description coupon.description = description
coupon.save() 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 @require_POST
......
...@@ -17,6 +17,7 @@ from django.core.urlresolvers import reverse ...@@ -17,6 +17,7 @@ from django.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.http import Http404, HttpResponse, HttpResponseNotFound from django.http import Http404, HttpResponse, HttpResponseNotFound
from django.conf import settings from django.conf import settings
from util.json_request import JsonResponse
from lms.lib.xblock.runtime import quote_slashes from lms.lib.xblock.runtime import quote_slashes
from xmodule_modifiers import wrap_xblock from xmodule_modifiers import wrap_xblock
...@@ -158,6 +159,7 @@ def _section_e_commerce(course, access): ...@@ -158,6 +159,7 @@ def _section_e_commerce(course, access):
'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}), 'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_purchase_transaction_url': reverse('get_purchase_transaction', kwargs={'course_id': course_key.to_deprecated_string()}), 'get_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_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()}), '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()}), '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()}), '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): ...@@ -183,15 +185,19 @@ def set_course_mode_price(request, course_id):
try: try:
course_price = int(request.POST['course_price']) course_price = int(request.POST['course_price'])
except ValueError: 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'] currency = request.POST['currency']
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_honor_mode = CourseMode.objects.filter(mode_slug='honor', course_id=course_key) course_honor_mode = CourseMode.objects.filter(mode_slug='honor', course_id=course_key)
if not course_honor_mode: if not course_honor_mode:
return HttpResponseNotFound( return JsonResponse(
_("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor') {'message': _("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor')},
) status=400) # status code 400: Bad Request
CourseModesArchive.objects.create( CourseModesArchive.objects.create(
course_id=course_id, mode_slug='honor', mode_display_name='Honor Code Certificate', 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'), 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): ...@@ -201,7 +207,7 @@ def set_course_mode_price(request, course_id):
min_price=course_price, min_price=course_price,
currency=currency currency=currency
) )
return HttpResponse(_("CourseMode price updated successfully")) return JsonResponse({'message': _("CourseMode price updated successfully")})
def _section_course_info(course, access): def _section_course_info(course, access):
......
...@@ -3,7 +3,10 @@ Student and course analytics. ...@@ -3,7 +3,10 @@ Student and course analytics.
Serve miscellaneous course and student data 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 from django.contrib.auth.models import User
import xmodule.graders as xmgraders import xmodule.graders as xmgraders
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -18,11 +21,77 @@ ORDER_FEATURES = ('purchase_time',) ...@@ -18,11 +21,77 @@ ORDER_FEATURES = ('purchase_time',)
SALE_FEATURES = ('total_amount', 'company_name', 'company_contact_name', 'company_contact_email', 'recipient_name', SALE_FEATURES = ('total_amount', 'company_name', 'company_contact_name', 'company_contact_email', 'recipient_name',
'recipient_email', 'customer_reference_number', 'internal_reference') '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 AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at') COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at')
COUPON_FEATURES = ('course_id', 'percentage_discount', 'description') 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): def sale_record_features(course_id, features):
""" """
Return list of sales features as dictionaries. Return list of sales features as dictionaries.
...@@ -73,7 +142,7 @@ def purchase_transactions(course_id, features): ...@@ -73,7 +142,7 @@ def purchase_transactions(course_id, features):
""" """
Return list of purchased transactions features as dictionaries. 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 [ would return [
{'username': 'username1', 'email': 'email1', unit_cost:'cost1 in decimal'.} {'username': 'username1', 'email': 'email1', unit_cost:'cost1 in decimal'.}
{'username': 'username2', 'email': 'email2', unit_cost:'cost2 in decimal'.} {'username': 'username2', 'email': 'email2', unit_cost:'cost2 in decimal'.}
......
...@@ -7,11 +7,11 @@ from student.models import CourseEnrollment ...@@ -7,11 +7,11 @@ from student.models import CourseEnrollment
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey 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 ( from instructor_analytics.basic import (
sale_record_features, enrolled_students_features, course_registration_features, coupon_codes_features, sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features,
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
) )
from course_groups.tests.helpers import CohortFactory from course_groups.tests.helpers import CohortFactory
from course_groups.models import CourseUserGroup from course_groups.models import CourseUserGroup
...@@ -137,6 +137,57 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): ...@@ -137,6 +137,57 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
self.assertEqual(sale_record['total_used_codes'], 0) self.assertEqual(sale_record['total_used_codes'], 0)
self.assertEqual(sale_record['total_codes'], 5) 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): class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
""" Test basic course registration codes analytics functions. """ """ Test basic course registration codes analytics functions. """
......
...@@ -21,6 +21,6 @@ def user_has_cart_context_processor(request): ...@@ -21,6 +21,6 @@ def user_has_cart_context_processor(request):
settings.FEATURES.get('ENABLE_SHOPPING_CART') and # settings enable shopping cart and settings.FEATURES.get('ENABLE_SHOPPING_CART') and # settings enable shopping cart and
shoppingcart.models.Order.user_cart_has_items( shoppingcart.models.Order.user_cart_has_items(
request.user, request.user,
shoppingcart.models.PaidCourseRegistration [shoppingcart.models.PaidCourseRegistration, shoppingcart.models.CourseRegCodeItem]
) # user's cart has PaidCourseRegistrations ) # user's cart has PaidCourseRegistrations
)} )}
# -*- 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']
\ No newline at end of file
...@@ -6,7 +6,9 @@ from decimal import Decimal ...@@ -6,7 +6,9 @@ from decimal import Decimal
import pytz import pytz
import logging import logging
import smtplib 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 boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
from django.dispatch import receiver from django.dispatch import receiver
from django.db import models from django.db import models
...@@ -19,6 +21,7 @@ from django.db import transaction ...@@ -19,6 +21,7 @@ from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from django.core.mail.message import EmailMessage
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -62,6 +65,19 @@ ORDER_STATUSES = ( ...@@ -62,6 +65,19 @@ ORDER_STATUSES = (
OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103 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): class Order(models.Model):
""" """
This is the model for an order. Before purchase, an Order and its related OrderItems are used 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): ...@@ -88,6 +104,15 @@ class Order(models.Model):
# a JSON dump of the CC processor response, for completeness # a JSON dump of the CC processor response, for completeness
processor_reply_dump = models.TextField(blank=True) 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 @classmethod
def get_cart_for_user(cls, user): def get_cart_for_user(cls, user):
""" """
...@@ -102,7 +127,7 @@ class Order(models.Model): ...@@ -102,7 +127,7 @@ class Order(models.Model):
return cart_order return cart_order
@classmethod @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 Returns true if the user (anonymous user ok) has
a cart with items in it. (Which means it should be displayed. a cart with items in it. (Which means it should be displayed.
...@@ -112,7 +137,17 @@ class Order(models.Model): ...@@ -112,7 +137,17 @@ class Order(models.Model):
if not user.is_authenticated(): if not user.is_authenticated():
return False return False
cart = cls.get_cart_for_user(user) 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 @property
def total_cost(self): def total_cost(self):
...@@ -130,17 +165,27 @@ class Order(models.Model): ...@@ -130,17 +165,27 @@ class Order(models.Model):
if not item_type: if not item_type:
return self.orderitem_set.exists() # pylint: disable=E1101 return self.orderitem_set.exists() # pylint: disable=E1101
else: else:
items = self.orderitem_set.all().select_subclasses() items = self.orderitem_set.all().select_subclasses() # pylint: disable=E1101
for item in items: for item in items:
if isinstance(item, item_type): if isinstance(item, item_type):
return True return True
return False 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): def clear(self):
""" """
Clear out all the items in the cart 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 @transaction.commit_on_success
def start_purchase(self): def start_purchase(self):
...@@ -158,6 +203,122 @@ class Order(models.Model): ...@@ -158,6 +203,122 @@ class Order(models.Model):
for item in OrderItem.objects.filter(order=self).select_subclasses(): for item in OrderItem.objects.filter(order=self).select_subclasses():
item.start_purchase() 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='', def purchase(self, first='', last='', street1='', street2='', city='', state='', postalcode='',
country='', ccnum='', cardtype='', processor_reply_dump=''): country='', ccnum='', cardtype='', processor_reply_dump=''):
""" """
...@@ -200,29 +361,48 @@ class Order(models.Model): ...@@ -200,29 +361,48 @@ class Order(models.Model):
# this should return all of the objects with the correct types of the # this should return all of the objects with the correct types of the
# subclasses # subclasses
orderitems = OrderItem.objects.filter(order=self).select_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: for item in orderitems:
item.purchase_item() item.purchase_item()
# send confirmation e-mail csv_file = None
subject = _("Order Payment Confirmation") courses_info = []
message = render_to_string( if self.order_type == OrderTypes.BUSINESS:
'emails/order_confirmation_email.txt', #
{ # Generate the CSV file that contains all of the RegistrationCodes that have already been
'order': self, # generated when the purchase has transacted
'order_items': orderitems, #
'has_billing_info': settings.FEATURES['STORE_BILLING_INFO'] csv_file, courses_info = self.generate_registration_codes_csv(orderitems, site_name)
}
)
try:
from_address = microsite.get_value(
'email_from_address',
settings.DEFAULT_FROM_EMAIL
)
send_mail(subject, message, self.send_confirmation_emails(orderitems, self.order_type == OrderTypes.BUSINESS, csv_file, site_name, courses_info)
from_address, [self.user.email]) # pylint: disable=E1101
except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually def add_billing_details(self, company_name='', company_contact_name='', company_contact_email='', recipient_name='',
log.error('Failed sending confirmation e-mail for order %d', self.id) # pylint: disable=E1101 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): def generate_receipt_instructions(self):
""" """
...@@ -421,6 +601,16 @@ class RegistrationCodeRedemption(models.Model): ...@@ -421,6 +601,16 @@ class RegistrationCodeRedemption(models.Model):
redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True) redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True)
@classmethod @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): def add_reg_code_redemption(cls, course_reg_code, order):
""" """
add course registration code info into RegistrationCodeRedemption model add course registration code info into RegistrationCodeRedemption model
...@@ -503,6 +693,16 @@ class CouponRedemption(models.Model): ...@@ -503,6 +693,16 @@ class CouponRedemption(models.Model):
coupon = models.ForeignKey(Coupon, db_index=True) coupon = models.ForeignKey(Coupon, db_index=True)
@classmethod @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): def get_discount_price(cls, percentage_discount, value):
""" """
return discounted price against coupon return discounted price against coupon
...@@ -665,6 +865,142 @@ class PaidCourseRegistration(OrderItem): ...@@ -665,6 +865,142 @@ class PaidCourseRegistration(OrderItem):
return u"" 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): class PaidCourseRegistrationAnnotation(models.Model):
""" """
A model that maps course_id to an additional annotation. This is specifically needed because when Stanford A model that maps course_id to an additional annotation. This is specifically needed because when Stanford
...@@ -1011,3 +1347,9 @@ class Donation(OrderItem): ...@@ -1011,3 +1347,9 @@ class Donation(OrderItem):
# The donation is for the organization as a whole, not a specific course # The donation is for the organization as a whole, not a specific course
else: else:
return _(u"Donation for {platform_name}").format(platform_name=settings.PLATFORM_NAME) 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 ( ...@@ -19,15 +19,17 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config ModuleStoreTestCase, mixed_store_config
) )
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from shoppingcart.models import ( from shoppingcart.models import (
Order, OrderItem, CertificateItem, Order, OrderItem, CertificateItem,
InvalidCartItem, PaidCourseRegistration, InvalidCartItem, CourseRegistrationCode, PaidCourseRegistration, CourseRegCodeItem,
Donation, OrderItemSubclassPK Donation, OrderItemSubclassPK
) )
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from course_modes.models import CourseMode 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 from opaque_keys.edx.locator import CourseLocator
...@@ -63,21 +65,21 @@ class OrderTest(ModuleStoreTestCase): ...@@ -63,21 +65,21 @@ class OrderTest(ModuleStoreTestCase):
item = OrderItem(order=cart, user=self.user) item = OrderItem(order=cart, user=self.user)
item.save() item.save()
self.assertTrue(Order.user_cart_has_items(self.user)) 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, [CertificateItem]))
self.assertFalse(Order.user_cart_has_items(self.user, PaidCourseRegistration)) self.assertFalse(Order.user_cart_has_items(self.user, [PaidCourseRegistration]))
def test_user_cart_has_paid_course_registration_items(self): def test_user_cart_has_paid_course_registration_items(self):
cart = Order.get_cart_for_user(self.user) cart = Order.get_cart_for_user(self.user)
item = PaidCourseRegistration(order=cart, user=self.user) item = PaidCourseRegistration(order=cart, user=self.user)
item.save() item.save()
self.assertTrue(Order.user_cart_has_items(self.user, PaidCourseRegistration)) self.assertTrue(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, [CertificateItem]))
def test_user_cart_has_certificate_items(self): def test_user_cart_has_certificate_items(self):
cart = Order.get_cart_for_user(self.user) cart = Order.get_cart_for_user(self.user)
CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
self.assertTrue(Order.user_cart_has_items(self.user, CertificateItem)) self.assertTrue(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, [PaidCourseRegistration]))
def test_cart_clear(self): def test_cart_clear(self):
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
...@@ -189,7 +191,7 @@ class OrderTest(ModuleStoreTestCase): ...@@ -189,7 +191,7 @@ class OrderTest(ModuleStoreTestCase):
def test_purchase_item_email_smtp_failure(self, error_logger): def test_purchase_item_email_smtp_failure(self, error_logger):
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') 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() cart.purchase()
self.assertTrue(error_logger.called) self.assertTrue(error_logger.called)
...@@ -326,6 +328,15 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): ...@@ -326,6 +328,15 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
self.assertEqual(self.cart.total_cost, self.cost) 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): def test_add_with_default_mode(self):
""" """
Tests add_to_cart where the mode specified in the argument is NOT in the database Tests add_to_cart where the mode specified in the argument is NOT in the database
...@@ -341,6 +352,31 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): ...@@ -341,6 +352,31 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
self.assertEqual(self.cart.total_cost, 0) self.assertEqual(self.cart.total_cost, 0)
self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_key)) 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): def test_purchased_callback(self):
reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
self.cart.purchase() self.cart.purchase()
...@@ -382,6 +418,12 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): ...@@ -382,6 +418,12 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
reg1.purchased_callback() reg1.purchased_callback()
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) 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): def test_user_cart_has_both_items(self):
""" """
This test exists b/c having both CertificateItem and PaidCourseRegistration in an order used to break 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 ...@@ -13,7 +13,8 @@ from django.test.utils import override_settings
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE 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 shoppingcart.views import initialize_report
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -203,6 +204,8 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): ...@@ -203,6 +204,8 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
course_mode2.save() course_mode2.save()
self.annotation = PaidCourseRegistrationAnnotation(course_id=self.course_key, annotation=self.TEST_ANNOTATION) self.annotation = PaidCourseRegistrationAnnotation(course_id=self.course_key, annotation=self.TEST_ANNOTATION)
self.annotation.save() 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.cart = Order.get_cart_for_user(self.user)
self.reg = PaidCourseRegistration.add_to_order(self.cart, self.course_key) 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') self.cert_item = CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified')
...@@ -269,3 +272,9 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): ...@@ -269,3 +272,9 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation 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)) 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))
""" """
Tests for Shopping Cart views Tests for Shopping Cart views
""" """
import json
from urlparse import urlparse from urlparse import urlparse
from decimal import Decimal
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings from django.conf import settings
...@@ -14,6 +12,7 @@ from django.utils.translation import ugettext as _ ...@@ -14,6 +12,7 @@ from django.utils.translation import ugettext as _
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.messages.storage.fallback import FallbackStorage from django.contrib.messages.storage.fallback import FallbackStorage
from django.core import mail
from django.core.cache import cache from django.core.cache import cache
from pytz import UTC from pytz import UTC
...@@ -28,7 +27,7 @@ from xmodule.modulestore.tests.django_utils import ( ...@@ -28,7 +27,7 @@ from xmodule.modulestore.tests.django_utils import (
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from shoppingcart.views import _can_download_report, _get_date_from_str from shoppingcart.views import _can_download_report, _get_date_from_str
from shoppingcart.models import ( from shoppingcart.models import (
Order, CertificateItem, PaidCourseRegistration, Order, CertificateItem, PaidCourseRegistration, CourseRegCodeItem,
Coupon, CourseRegistrationCode, RegistrationCodeRedemption, Coupon, CourseRegistrationCode, RegistrationCodeRedemption,
DonationConfiguration DonationConfiguration
) )
...@@ -41,6 +40,8 @@ from shoppingcart.processors import render_purchase_form_html ...@@ -41,6 +40,8 @@ from shoppingcart.processors import render_purchase_form_html
from shoppingcart.admin import SoftDeleteCouponAdmin from shoppingcart.admin import SoftDeleteCouponAdmin
from shoppingcart.views import initialize_report from shoppingcart.views import initialize_report
from shoppingcart.tests.payment_fake import PaymentFakeView from shoppingcart.tests.payment_fake import PaymentFakeView
from decimal import Decimal
import json
def mock_render_purchase_form_html(*args, **kwargs): def mock_render_purchase_form_html(*args, **kwargs):
...@@ -133,6 +134,30 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -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()])) resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()]))
self.assertEqual(resp.status_code, 403) 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): def test_add_course_to_cart_already_in_cart(self):
PaidCourseRegistration.add_to_order(self.cart, self.course_key) PaidCourseRegistration.add_to_order(self.cart, self.course_key)
self.login_user() self.login_user()
...@@ -148,6 +173,120 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -148,6 +173,120 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
self.assertIn("Discount does not exist against code '{0}'.".format(non_existing_code), resp.content) 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): def test_course_discount_invalid_reg_code(self):
self.add_reg_code(self.course_key) self.add_reg_code(self.course_key)
self.add_course_to_user_cart(self.course_key) self.add_course_to_user_cart(self.course_key)
...@@ -320,6 +459,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -320,6 +459,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id)) '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') @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): def test_existing_reg_code_redemption_on_removing_item(self, info_log):
self.add_reg_code(self.course_key) self.add_reg_code(self.course_key)
...@@ -474,14 +643,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -474,14 +643,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200) 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() 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(reg_item, purchase_form_arg_cart_items)
self.assertIn(cert_item, purchase_form_arg_cart_items) self.assertIn(cert_item, purchase_form_arg_cart_items)
self.assertEqual(len(purchase_form_arg_cart_items), 2) self.assertEqual(len(purchase_form_arg_cart_items), 2)
((template, context), _) = render_mock.call_args ((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(len(context['shoppingcart_items']), 2)
self.assertEqual(context['amount'], 80) self.assertEqual(context['amount'], 80)
self.assertIn("80.00", context['form_html']) self.assertIn("80.00", context['form_html'])
...@@ -626,7 +795,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -626,7 +795,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) 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') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id]))
...@@ -665,14 +834,59 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -665,14 +834,59 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertIn('FirstNameTesting123', resp.content) self.assertIn('FirstNameTesting123', resp.content)
self.assertIn('80.00', 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(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item, context['order_items']) self.assertIn(reg_item, context['shoppingcart_items'][0])
self.assertIn(cert_item, context['order_items']) self.assertIn(cert_item, context['shoppingcart_items'][1])
self.assertFalse(context['any_refunds']) self.assertFalse(context['any_refunds'])
@patch('shoppingcart.views.render_to_response', render_mock) @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): def test_show_receipt_success_with_upgrade(self):
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
...@@ -705,8 +919,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -705,8 +919,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item, context['order_items']) self.assertIn(reg_item, context['shoppingcart_items'][0])
self.assertIn(cert_item, context['order_items']) self.assertIn(cert_item, context['shoppingcart_items'][1])
self.assertFalse(context['any_refunds']) self.assertFalse(context['any_refunds'])
course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key)
...@@ -736,8 +950,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -736,8 +950,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
((template, context), _tmp) = render_mock.call_args ((template, context), _tmp) = render_mock.call_args
self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item, context['order_items']) self.assertIn(reg_item, context['shoppingcart_items'][0])
self.assertIn(cert_item, context['order_items']) self.assertIn(cert_item, context['shoppingcart_items'][1])
self.assertTrue(context['any_refunds']) self.assertTrue(context['any_refunds'])
@patch('shoppingcart.views.render_to_response', render_mock) @patch('shoppingcart.views.render_to_response', render_mock)
...@@ -869,6 +1083,12 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): ...@@ -869,6 +1083,12 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'}) 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) 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) @override_settings(MODULESTORE=MODULESTORE_CONFIG)
@ddt.ddt @ddt.ddt
......
...@@ -17,6 +17,9 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']: ...@@ -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'^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'^register/redeem/(?P<registration_code>[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'),
url(r'^use_code/$', 'use_code'), 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'), url(r'^register_courses/$', 'register_courses'),
) )
......
...@@ -9,12 +9,13 @@ from django.http import ( ...@@ -9,12 +9,13 @@ from django.http import (
HttpResponseBadRequest, HttpResponseForbidden, Http404 HttpResponseBadRequest, HttpResponseForbidden, Http404
) )
from django.utils.translation import ugettext as _ 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.views.decorators.http import require_POST, require_http_methods
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from microsite_configuration import microsite
from util.bad_request_rate_limiter import BadRequestRateLimiter from util.bad_request_rate_limiter import BadRequestRateLimiter
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from microsite_configuration import microsite
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
...@@ -31,7 +32,8 @@ from .exceptions import ( ...@@ -31,7 +32,8 @@ from .exceptions import (
MultipleCouponsNotAllowedException, InvalidCartItem MultipleCouponsNotAllowedException, InvalidCartItem
) )
from .models import ( from .models import (
Order, PaidCourseRegistration, OrderItem, Coupon, Order, OrderTypes,
PaidCourseRegistration, OrderItem, Coupon, CourseRegCodeItem,
CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption, CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption,
Donation, DonationConfiguration Donation, DonationConfiguration
) )
...@@ -39,6 +41,7 @@ from .processors import ( ...@@ -39,6 +41,7 @@ from .processors import (
process_postpay_callback, render_purchase_form_html, process_postpay_callback, render_purchase_form_html,
get_signed_purchase_params, get_purchase_endpoint get_signed_purchase_params, get_purchase_endpoint
) )
import json import json
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
...@@ -91,21 +94,67 @@ def add_course_to_cart(request, course_id): ...@@ -91,21 +94,67 @@ def add_course_to_cart(request, course_id):
@login_required @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): def show_cart(request):
"""
This view shows cart items.
"""
cart = Order.get_cart_for_user(request.user) cart = Order.get_cart_for_user(request.user)
total_cost = cart.total_cost 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( callback_url = request.build_absolute_uri(
reverse("shoppingcart.views.postpay_callback") reverse("shoppingcart.views.postpay_callback")
) )
form_html = render_purchase_form_html(cart, callback_url=callback_url) form_html = render_purchase_form_html(cart, callback_url=callback_url)
context = { context = {
'shoppingcart_items': cart_items, 'order': cart,
'shoppingcart_items': shoppingcart_items,
'amount': total_cost, 'amount': total_cost,
'site_name': site_name,
'form_html': form_html, 'form_html': form_html,
} }
return render_to_response("shoppingcart/list.html", context) return render_to_response("shoppingcart/shopping_cart.html", context)
@login_required @login_required
...@@ -127,22 +176,26 @@ def clear_cart(request): ...@@ -127,22 +176,26 @@ def clear_cart(request):
@login_required @login_required
def remove_item(request): 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') 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: if item.user == request.user:
order_item_course_id = None order_item_course_id = getattr(item, 'course_id')
if hasattr(item, 'paidcourseregistration'):
order_item_course_id = item.paidcourseregistration.course_id
item.delete() item.delete()
log.info('order item {0} removed for user {1}'.format(item_id, request.user)) 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) 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') return HttpResponse('OK')
def remove_code_redemption(order_item_course_id, item_id, item, user): def remove_code_redemption(order_item_course_id, item_id, item, user):
""" """
If an item removed from shopping cart then we will remove If an item removed from shopping cart then we will remove
...@@ -159,6 +212,8 @@ def remove_code_redemption(order_item_course_id, item_id, item, user): ...@@ -159,6 +212,8 @@ 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}"' log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(coupon_redemption.coupon.code, user, item_id)) .format(coupon_redemption.coupon.code, user, item_id))
except CouponRedemption.DoesNotExist: except CouponRedemption.DoesNotExist:
pass
try: try:
# Try to remove redemption information of registration code, If exist. # Try to remove redemption information of registration code, If exist.
reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id) reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id)
...@@ -172,6 +227,18 @@ def remove_code_redemption(order_item_course_id, item_id, item, user): ...@@ -172,6 +227,18 @@ def remove_code_redemption(order_item_course_id, item_id, item, user):
@login_required @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
def use_code(request): def use_code(request):
""" """
This method may generate the discount against valid coupon code This method may generate the discount against valid coupon code
...@@ -448,6 +515,49 @@ def postpay_callback(request): ...@@ -448,6 +515,49 @@ def postpay_callback(request):
return render_to_response('shoppingcart/error.html', {'order': result['order'], return render_to_response('shoppingcart/error.html', {'order': result['order'],
'error_html': result['error_html']}) '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 @login_required
def show_receipt(request, ordernum): def show_receipt(request, ordernum):
""" """
...@@ -464,29 +574,61 @@ def show_receipt(request, ordernum): ...@@ -464,29 +574,61 @@ def show_receipt(request, ordernum):
raise Http404('Order not found!') raise Http404('Order not found!')
order_items = OrderItem.objects.filter(order=order).select_subclasses() 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) any_refunds = any(i.status == "refunded" for i in order_items)
receipt_template = 'shoppingcart/receipt.html' receipt_template = 'shoppingcart/receipt.html'
__, instructions = order.generate_receipt_instructions() __, instructions = order.generate_receipt_instructions()
# we want to have the ability to override the default receipt page when order_type = getattr(order, 'order_type')
# there is only one item in the order
# Only orders where order_items.count() == 1 might be attempting to upgrade
attempting_upgrade = request.session.get('attempting_upgrade', False)
if attempting_upgrade:
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, order_items[0].course_id)
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 = { context = {
'order': order, 'order': order,
'order_items': order_items, 'shoppingcart_items': shoppingcart_items,
'any_refunds': any_refunds, 'any_refunds': any_refunds,
'instructions': instructions, '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: if order_items.count() == 1:
receipt_template = order_items[0].single_item_receipt_template receipt_template = order_items[0].single_item_receipt_template
context.update(order_items[0].single_item_receipt_context) context.update(order_items[0].single_item_receipt_context)
# Only orders where order_items.count() == 1 might be attempting to upgrade
attempting_upgrade = request.session.get('attempting_upgrade', False)
if attempting_upgrade:
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, order_items[0].course_id)
course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED)
request.session['attempting_upgrade'] = False
return render_to_response(receipt_template, context) return render_to_response(receipt_template, context)
......
...@@ -11,6 +11,7 @@ class ECommerce ...@@ -11,6 +11,7 @@ class ECommerce
# gather elements # gather elements
@$list_purchase_csv_btn = @$section.find("input[name='list-purchase-transaction-csv']'") @$list_purchase_csv_btn = @$section.find("input[name='list-purchase-transaction-csv']'")
@$list_sale_csv_btn = @$section.find("input[name='list-sale-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']'") @$download_company_name = @$section.find("input[name='download_company_name']'")
@$active_company_name = @$section.find("input[name='active_company_name']'") @$active_company_name = @$section.find("input[name='active_company_name']'")
@$spent_company_name = @$section.find('input[name="spent_company_name"]') @$spent_company_name = @$section.find('input[name="spent_company_name"]')
...@@ -35,6 +36,10 @@ class ECommerce ...@@ -35,6 +36,10 @@ class ECommerce
url += '/csv' url += '/csv'
location.href = url 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) => @$download_coupon_codes.click (e) =>
url = @$download_coupon_codes.data 'endpoint' url = @$download_coupon_codes.data 'endpoint'
location.href = url location.href = url
......
...@@ -55,6 +55,15 @@ span { ...@@ -55,6 +55,15 @@ span {
font: inherit; font: inherit;
} }
.text-center {
text-align: center;
}
.text-dark-grey {
color: $dark-gray1;
font-size: 24px;
}
p + p, ul + p, ol + p { p + p, ul + p, ol + p {
margin-top: 20px; margin-top: 20px;
} }
......
...@@ -421,3 +421,14 @@ $header-sans-serif: 'Open Sans', Arial, Helvetica, sans-serif; ...@@ -421,3 +421,14 @@ $header-sans-serif: 'Open Sans', Arial, Helvetica, sans-serif;
// SPLINT: colors // SPLINT: colors
$msg-bg: $action-primary-bg; $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"] { ...@@ -1144,6 +1144,9 @@ input[name="subject"] {
} }
#e-commerce{ #e-commerce{
input[name='list-order-sale-csv'] {
margin-right: 14px;
}
input { input {
margin-bottom: 1em; margin-bottom: 1em;
line-height: 1.3em; line-height: 1.3em;
...@@ -1292,22 +1295,20 @@ input[name="subject"] { ...@@ -1292,22 +1295,20 @@ input[name="subject"] {
width: 650px; width: 650px;
margin-left: -325px; margin-left: -325px;
border-radius: 2px; border-radius: 2px;
input[type="submit"]#update_coupon_button{ input[type="button"]#update_coupon_button, input[type="button"]#add_coupon_button,
@include button(simple, $blue); input[type="button"]#set_course_button {
@extend .button-reset;
}
input[type="submit"]#add_coupon_button{
@include button(simple, $blue); @include button(simple, $blue);
@extend .button-reset; @extend .button-reset;
display: block;
height: auto;
margin: 0 auto;
width: 100%;
white-space: normal;
} }
input[name="generate-registration-codes-csv"]{ input[name="generate-registration-codes-csv"]{
@include button(simple, $blue); @include button(simple, $blue);
@extend .button-reset; @extend .button-reset;
} }
input[type="submit"]#set_course_button{
@include button(simple, $blue);
@extend .button-reset;
}
.modal-form-error { .modal-form-error {
box-shadow: inset 0 -1px 2px 0 #f3d9db; box-shadow: inset 0 -1px 2px 0 #f3d9db;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
......
...@@ -210,7 +210,7 @@ ...@@ -210,7 +210,7 @@
} }
} }
.enrollment-text { .enrollment-text {
color: #4A4A46; color: #9b9b93;
font-family: 'Open Sans',Verdana,Geneva,sans; font-family: 'Open Sans',Verdana,Geneva,sans;
line-height: normal; line-height: normal;
a { a {
...@@ -265,3 +265,654 @@ ...@@ -265,3 +265,654 @@
font-size: 24px; 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;
}
}
}
}
}
<%! 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)} ...@@ -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.")} ${_("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)} ${_("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 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: % 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 % 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)} ${_("Your order number is: {order_number}").format(order_number=order.id)}
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/> <%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"> <div class="inner-wrapper">
<button class="close-modal"> <button class="close-modal">
<i class="icon-remove"></i> <i class="icon-remove"></i>
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
${_("Please enter Coupon detail below")}</p> ${_("Please enter Coupon detail below")}</p>
</div> </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> <div id="coupon_form_error" class="modal-form-error"></div>
<fieldset class="group group-form group-form-requiredinformation"> <fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend> <legend class="is-hidden">${_("Required Information")}</legend>
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
</fieldset> </fieldset>
<div class="submit"> <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> </div>
</form> </form>
</div> </div>
......
...@@ -68,8 +68,12 @@ ...@@ -68,8 +68,12 @@
<div class="wrap"> <div class="wrap">
<h2>${_("Sales")}</h2> <h2>${_("Sales")}</h2>
<div> <div>
<span class="csv_tip">${_("Click to generate a CSV file for all sales records in this course")} <span class="csv_tip">
<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> <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> </span>
<hr> <hr>
<p>${_("Enter the invoice number to invalidate or re-validate sale")}</p> <p>${_("Enter the invoice number to invalidate or re-validate sale")}</p>
...@@ -384,8 +388,28 @@ ...@@ -384,8 +388,28 @@
modal_overLay.hide(); modal_overLay.hide();
}); });
$('#edit_coupon_form').submit(function () { $('#update_coupon_button').click(function () {
$("#update_coupon_button").attr('disabled', true); $("#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 () { $('#course_price_link').click(function () {
reset_input_fields(); reset_input_fields();
...@@ -397,7 +421,7 @@ ...@@ -397,7 +421,7 @@
reset_input_fields(); reset_input_fields();
$('input[name="generate-registration-codes-csv"]').removeAttr('disabled'); $('input[name="generate-registration-codes-csv"]').removeAttr('disabled');
}); });
$('#set_price_form').submit(function () { $('#set_course_button').click(function () {
$("#set_course_button").attr('disabled', true); $("#set_course_button").attr('disabled', true);
// Get the Code and Discount value and trim it // Get the Code and Discount value and trim it
var course_price = $.trim($('#mode_price').val()); var course_price = $.trim($('#mode_price').val());
...@@ -422,12 +446,31 @@ ...@@ -422,12 +446,31 @@
$("#set_course_button").removeAttr('disabled'); $("#set_course_button").removeAttr('disabled');
return false; 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); $("#add_coupon_button").attr('disabled', true);
// Get the Code and Discount value and trim it // Get the Code and Discount value and trim it
var code = $.trim($('#coupon_code').val()); var code = $.trim($('#coupon_code').val());
var coupon_discount = $.trim($('#coupon_discount').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 // Check if empty of not
if (code === '') { if (code === '') {
...@@ -448,36 +491,25 @@ ...@@ -448,36 +491,25 @@
$('#add_coupon_form #coupon_form_error').text("${_('Please enter the numeric value for discount')}"); $('#add_coupon_form #coupon_form_error').text("${_('Please enter the numeric value for discount')}");
return false; return false;
} }
}); $.ajax({
type: "POST",
$('#set_price_form').on('ajax:complete', function (event, xhr) { data: {
if (xhr.status == 200) { "code" : code,
location.reload(true); "discount": coupon_discount,
} else { "course_id": course_id,
$("#set_course_button").removeAttr('disabled'); "description": description
$('#set_price_form #course_form_error').attr('style', 'display: block !important'); },
$('#set_price_form #course_form_error').text(xhr.responseText); url: "${section_data['ajax_add_coupon']}",
} success: function (data) {
});
$('#add_coupon_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true); location.reload(true);
} else { },
$("#add_coupon_button").removeAttr('disabled'); 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').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text(xhr.responseText); $('#add_coupon_form #coupon_form_error').text(data.message);
$("#add_coupon_button").removeAttr('disabled');
} }
}); });
$('#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);
}
}); });
// removing close link's default behavior // removing close link's default behavior
$('.close-modal').click(function (e) { $('.close-modal').click(function (e) {
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<div class="submit"> <div class="submit">
<input type="hidden" name="coupon_id" id="coupon_id"/> <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> </div>
</form> </form>
</div> </div>
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
${_("Please enter Course Mode detail below")}</p> ${_("Please enter Course Mode detail below")}</p>
</div> </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> <div id="course_form_error" class="modal-form-error"></div>
<fieldset class="group group-form group-form-requiredinformation"> <fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend> <legend class="is-hidden">${_("Required Information")}</legend>
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
</ol> </ol>
</fieldset> </fieldset>
<div class="submit"> <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> </div>
</form> </form>
</div> </div>
......
<%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 @@ ...@@ -3,5 +3,5 @@
<input type="hidden" name="${pk}" value="${pv}" /> <input type="hidden" name="${pk}" value="${pv}" />
% endfor % endfor
<input type="submit" value="Check Out" /> <i class="icon-caret-right"></i><input type="submit" value="Payment"/>
</form> </form>
\ No newline at end of file
<%! 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>&nbsp;</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>
\ No newline at end of file
<%inherit file="shopping_cart_flow.html" />
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! 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':
<%block name="bodyclass">purchase-receipt</%block> <li>${_('Billing Details')}</li>
%endif
<%block name="pagetitle">${_("Register for [Course Name] | Receipt (Order")} ${order.id})</%block> </%block>
<%block name="content"> <%block name="confirmation_highlight">class="active"</%block>
<%block name="custom_content">
<div class="container"> <div class="container">
<section class="notification"> <section class="notification">
<h2>${_("Thank you for your Purchase!")}</h2> <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> % if (len(shoppingcart_items) == 1 and order_type == 'personal') or receipt_has_donation_item:
% for inst in instructions: % for inst in instructions:
<p>${inst}</p> <p>${inst}</p>
% endfor % endfor
% endif
</section> </section>
<section class="wrapper confirm-enrollment shopping-cart print">
<div class="gray-bg">
<div class="message-left">
<% courses_url = reverse('courses') %>
% if order_type == 'personal':
## 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
<section class="wrapper cart-list"> </div>
<div class="wrapper-content-main"> </div>
<article class="content-main"> % if order_type == 'business':
<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> <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>
<hr /> <table class="course-receipt">
<thead>
<table class="order-receipt"> <th>${_("Course Name")}</th>
<th>${_("Enrollment Code")}</th>
<th>${_("Enrollment Link")}</th>
</thead>
<tbody> <tbody>
% for registration_code in registration_codes:
<% course = get_course_by_id(registration_code.course_id, depth=0) %>
<tr> <tr>
<td colspan="2"><h3 class="order-number">${_("Order #")}${order.id}</h3></td> <td>${_("{course_name}").format(course_name=course.display_name)}</td>
<td></td> <td>${registration_code.code}</td>
<td colspan="2"><h3 class="order-date">${_("Date:")} ${order.purchase_time.date().isoformat()}</h3></td>
</tr> <% redemption_url = reverse('register_code_redemption', args = [registration_code.code] ) %>
<tr> <% enrollment_url = '{base_url}{redemption_url}'.format(base_url=site_name, redemption_url=redemption_url) %>
<td colspan="5"><h2 class="items-ordered">${_("Items ordered:")}</h2></td> <td><a href="${redemption_url}">${enrollment_url}</a></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> </tr>
% for item in order_items: % 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>
<tr class="order-item"> <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": % if item.status == "purchased":
<td>${item.qty}</td> <div class="col-1">
<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: % if item.list_price != None:
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span> <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 % endif
</td> </div>
<td>${"{0:0.2f}".format(item.line_cost)}</td> <div class="col-2">
<td>${item.currency.upper()}</td></tr> <div class="numbers-row">
<label>${_("Students")}:</label>
<div class="counter no-border text-dark-grey">
${item.qty}
</div>
</div>
</div>
% elif item.status == "refunded": % elif item.status == "refunded":
<td><del>${item.qty}</del></td> <div class="col-1">
<td><del>${item.line_desc}</del></td> % if item.list_price != None:
<td><del>${"{0:0.2f}".format(item.unit_cost)}</del></td> <div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span>
<td><del>${"{0:0.2f}".format(item.line_cost)}</del></td> </div>
<td><del>${item.currency.upper()}</del></td></tr> <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 % 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 % endfor
<tr> <div class="discount">
<td colspan="3"></td> <div class="code-text">
<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: % if any_refunds:
<p> <span>
## Translators: Please keep the "<del>" and "</del>" tags around your translation of the word "this" in your translation. ## 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.")} ${_("Note: items with strikethough like <del>this</del> have been refunded.")}
</p> </span>
% 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 % endif
</article> <span class="pull-right">${_("Total")}: <b>$${"{0:0.2f}".format(order.total_cost)} USD</b></span>
</div>
</div> </div>
</section> </section>
</div> </div>
......
<%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>
<%!
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 @@ ...@@ -103,7 +103,7 @@
</thead> </thead>
<tbody> <tbody>
% for item in order_items: % for item, course in shoppingcart_items:
<tr> <tr>
<td>${item.line_desc}</td> <td>${item.line_desc}</td>
<td> <td>
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
</thead> </thead>
<tbody> <tbody>
% for item in order_items: % for item, course in shoppingcart_items:
<tr> <tr>
% if item.status == "purchased": % if item.status == "purchased":
<td>${order.id}</td> <td>${order.id}</td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment