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