Commit 4f7c4949 by asadiqbal08 Committed by Chris Dodge

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
parent 5a31005e
......@@ -446,6 +446,10 @@ 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:
# 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
......
......@@ -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,11 +277,21 @@ 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())
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
......@@ -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
)}
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseRegCodeItem'
db.create_table('shoppingcart_courseregcodeitem', (
('orderitem_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['shoppingcart.OrderItem'], unique=True, primary_key=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=128, db_index=True)),
('mode', self.gf('django.db.models.fields.SlugField')(default='honor', max_length=50)),
))
db.send_create_signal('shoppingcart', ['CourseRegCodeItem'])
# Adding model 'CourseRegCodeItemAnnotation'
db.create_table('shoppingcart_courseregcodeitemannotation', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=128, db_index=True)),
('annotation', self.gf('django.db.models.fields.TextField')(null=True)),
))
db.send_create_signal('shoppingcart', ['CourseRegCodeItemAnnotation'])
# Adding field 'Order.company_name'
db.add_column('shoppingcart_order', 'company_name',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'Order.company_contact_name'
db.add_column('shoppingcart_order', 'company_contact_name',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'Order.company_contact_email'
db.add_column('shoppingcart_order', 'company_contact_email',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'Order.recipient_name'
db.add_column('shoppingcart_order', 'recipient_name',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'Order.recipient_email'
db.add_column('shoppingcart_order', 'recipient_email',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
keep_default=False)
# Adding field 'Order.customer_reference_number'
db.add_column('shoppingcart_order', 'customer_reference_number',
self.gf('django.db.models.fields.CharField')(max_length=63, null=True, blank=True),
keep_default=False)
# Adding field 'Order.order_type'
db.add_column('shoppingcart_order', 'order_type',
self.gf('django.db.models.fields.CharField')(default='personal', max_length=32),
keep_default=False)
def backwards(self, orm):
# Deleting model 'CourseRegCodeItem'
db.delete_table('shoppingcart_courseregcodeitem')
# Deleting model 'CourseRegCodeItemAnnotation'
db.delete_table('shoppingcart_courseregcodeitemannotation')
# Deleting field 'Order.company_name'
db.delete_column('shoppingcart_order', 'company_name')
# Deleting field 'Order.company_contact_name'
db.delete_column('shoppingcart_order', 'company_contact_name')
# Deleting field 'Order.company_contact_email'
db.delete_column('shoppingcart_order', 'company_contact_email')
# Deleting field 'Order.recipient_name'
db.delete_column('shoppingcart_order', 'recipient_name')
# Deleting field 'Order.recipient_email'
db.delete_column('shoppingcart_order', 'recipient_email')
# Deleting field 'Order.customer_reference_number'
db.delete_column('shoppingcart_order', 'customer_reference_number')
# Deleting field 'Order.order_type'
db.delete_column('shoppingcart_order', 'order_type')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'shoppingcart.certificateitem': {
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
},
'shoppingcart.coupon': {
'Meta': {'object_name': 'Coupon'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'shoppingcart.couponredemption': {
'Meta': {'object_name': 'CouponRedemption'},
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.courseregcodeitem': {
'Meta': {'object_name': 'CourseRegCodeItem', '_ormbases': ['shoppingcart.OrderItem']},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
},
'shoppingcart.courseregcodeitemannotation': {
'Meta': {'object_name': 'CourseRegCodeItemAnnotation'},
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'shoppingcart.courseregistrationcode': {
'Meta': {'object_name': 'CourseRegistrationCode'},
'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"})
},
'shoppingcart.donation': {
'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
},
'shoppingcart.donationconfiguration': {
'Meta': {'object_name': 'DonationConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'shoppingcart.invoice': {
'Meta': {'object_name': 'Invoice'},
'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'total_amount': ('django.db.models.fields.FloatField', [], {}),
'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'})
},
'shoppingcart.order': {
'Meta': {'object_name': 'Order'},
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order_type': ('django.db.models.fields.CharField', [], {'default': "'personal'", 'max_length': '32'}),
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.orderitem': {
'Meta': {'object_name': 'OrderItem'},
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'shoppingcart.paidcourseregistration': {
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
},
'shoppingcart.paidcourseregistrationannotation': {
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'shoppingcart.registrationcoderedemption': {
'Meta': {'object_name': 'RegistrationCodeRedemption'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}),
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 16, 0, 0)', 'null': 'True'}),
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['shoppingcart']
\ No newline at end of file
......@@ -6,7 +6,9 @@ from decimal import Decimal
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):
"""
......@@ -421,6 +601,16 @@ class RegistrationCodeRedemption(models.Model):
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):
"""
add course registration code info into RegistrationCodeRedemption model
......@@ -503,6 +693,16 @@ class CouponRedemption(models.Model):
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):
"""
return discounted price against coupon
......@@ -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))
"""
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)
......@@ -320,6 +459,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id))
@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):
self.add_reg_code(self.course_key)
......@@ -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,14 +834,59 @@ 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):
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
......@@ -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
......@@ -91,21 +94,67 @@ def add_course_to_cart(request, course_id):
@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,6 +212,8 @@ def remove_code_redemption(order_item_course_id, item_id, item, user):
log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(coupon_redemption.coupon.code, user, item_id))
except CouponRedemption.DoesNotExist:
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)
......@@ -172,6 +227,18 @@ def remove_code_redemption(order_item_course_id, item_id, item, user):
@login_required
def reset_code_redemption(request):
"""
This method reset the code redemption from user cart items.
"""
cart = Order.get_cart_for_user(request.user)
cart.reset_cart_items_prices()
CouponRedemption.delete_coupon_redemption(request.user, cart)
RegistrationCodeRedemption.delete_registration_redemption(request.user, cart)
return HttpResponse('reset')
@login_required
def use_code(request):
"""
This method may generate the discount against valid coupon code
......@@ -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,29 +574,61 @@ 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
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)
if attempting_upgrade:
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, order_items[0].course_id)
course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED)
request.session['attempting_upgrade'] = False
recipient_list = []
registration_codes = None
total_registration_codes = None
recipient_list.append(getattr(order.user, 'email'))
if order_type == OrderTypes.BUSINESS:
registration_codes = CourseRegistrationCode.objects.filter(order=order)
total_registration_codes = registration_codes.count()
if order.company_contact_email:
recipient_list.append(order.company_contact_email)
if order.recipient_email:
recipient_list.append(order.recipient_email)
appended_recipient_emails = ", ".join(recipient_list)
context = {
'order': order,
'order_items': order_items,
'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)
# Only orders where order_items.count() == 1 might be attempting to upgrade
attempting_upgrade = request.session.get('attempting_upgrade', False)
if attempting_upgrade:
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, order_items[0].course_id)
course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED)
request.session['attempting_upgrade'] = False
return render_to_response(receipt_template, context)
......
......@@ -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;
......
......@@ -210,7 +210,7 @@
}
}
.enrollment-text {
color: #4A4A46;
color: #9b9b93;
font-family: 'Open Sans',Verdana,Geneva,sans;
line-height: normal;
a {
......@@ -265,3 +265,654 @@
font-size: 24px;
}
}
.shopping-cart{
a.blue{
display: inline-block;
background: $blue2;
color: white;
padding: 20px 40px;
border-radius: 3px;
font-size: 24px;
font-weight: 400;
margin: 10px 0px 20px;
&:hover{
text-decoration: none;
}
}
.relative{
position: relative;
}
input[type="text"], input[type="email"] , select{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-style: normal;
border: 2px solid $dark-gray2;
height: auto;
padding: 8px 12px;
font-weight: 600;
width: 260px;
font-size: 16px;
&:focus{
border-color: $dark-gray2;
box-shadow: none;
outline: none;
}
&.error{
border-color: $red1;
}
}
.hidden{display: none;}
.show{display: inline-block;}
h1{
font-size: 24px;
color: $dark-gray1;
text-align: left;
padding: 15px 0px;
margin: 10px 0 0 0;
letter-spacing: 0px;
}
ul.steps{
padding: 0px;
margin: 0;
list-style: none;
border-top: 3px solid $light-gray1;
border-bottom: 3px solid $light-gray1;
li{
display: inline-block;
padding: 26px 30px;
margin: 0px 30px;
font-size: 20px;
font-weight: 100;
position: relative;
color: $dark-gray1;
&.active{font-weight: 400; border-bottom: 3px solid $light-gray1;}
&:first-child {padding-left: 30px;margin-left: 0;}
&:last-child {
padding-right: 30px;margin-right: 0;
&:after{display: none;}
}
&:after{
content: "\f178";
position: absolute;
font-family: FontAwesome;
right: -40px;
color: #ddd;
font-weight: 100;
}
}
}
hr{border-top: 1px solid $dark-gray2;}
.user-data{
margin: 20px 0px;
.image{
width: 220px;
float: left;
img{
width: 100%;
height: auto;
}
}
.data-input{
width: calc(100% - 245px);
float: left;
margin-left: 25px;
h3, h3 span{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-size: 16px;
text-transform: uppercase;
color: $light-gray2;
padding: 0;
}
h1, h1 span{
font-size: 24px;
color: $dark-gray1;
padding: 0 0 10px 0;
text-transform: capitalize;
span{font-size: 16px;}
}
hr{border-top: 1px solid $dark-gray2;}
.three-col{
.col-1{
width: 450px;
float: left;
font-size: 16px;
text-transform: uppercase;
color: $light-gray2;
.price{
span{
color: $dark-gray1;
font-size: 24px;
padding-left: 20px;
}
&.green{color: $green1;}
.line-through{text-decoration: line-through;}
}
}
.col-2{
width: 350px;
float: left;
line-height: 44px;
text-transform: uppercase;
color: $light-gray2;
.numbers-row{
position: relative;
label{
font-size: 16px;
text-transform: uppercase;
color: $light-gray2;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-weight: 400;
font-style: normal;
}
.counter{
margin-left: 25px;
border-radius: 3px;
padding: 6px 30px 6px 10px;
display: inline-block;
border: 2px solid $dark-gray2;
input[type="text"]{
width: 75px;
border: none;
box-shadow: none;
color: #666;
font-size: 25px;
font-style: normal;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-weight: 600;
padding: 8px 0;
height: auto;
text-align: center;
&:focus{
outline: none;
}
}
}
.button{
position: absolute;
background: none;
margin-left: -30px;
padding: 0;
border: none;
box-shadow: none;
text-shadow: none;
height: 17px;
i{
color: $dark-gray2;
font-size: 24px;
span{display: none;}
}
&.inc{top: 9px;}
&.dec{top: 30px;height: 22px;}
}
&.disabled{
.counter{
border: 2px solid #CCCCCC;
&:hover{
cursor: not-allowed;
}
input{
color: #CCC;
}
}
.button{
i{
color: #ccc;
}
}
}
.updateBtn{
display: inline-block;
float: right;
font-size: 15px;
padding: 25px 35px 25px 0;
&:focus{
outline: none;
}
}
span.error-text{
display: block;
text-transform: lowercase;
}
}
.disable-numeric-counter{
pointer-events: none;
}
}
.col-3{
width: 100px;
float: right;
a.btn-remove{
float: right;
opacity: 0.8;
i{
color: $dark-gray2;
font-size: 24PX;
line-height: 40px;
}
&:hover{text-decoration: none;opacity: 1;}
}
}
}
}
}
.discount{
border-bottom: 2px solid $light-gray1;
border-top: 2px solid $light-gray1;
margin: 20px 0;
padding: 17px 20px 15px;
min-height: 45px;
.code-text{
a{
color: $blue1;
font-size: 18px;
text-transform: lowercase;
font-weight: 600;
display: inline-block;
padding: 10px 0px;
cursor: pointer;
}
span{
display: inline-block;
padding: 9px 0px;
b{
font-weight: 600;
font-size: 24px;
padding-left: 20px;
letter-spacing: 0;
}
}
}
.code-input{
display: inline-block;
input[type="text"]{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-style: normal;
border: 2px solid $dark-gray2;
height: auto;
padding: 8px 12px;
font-weight: 600;
width: 260px;
&:focus{
border-color: $dark-gray2;
box-shadow: none;
}
&.error{
border-color: $red1;
}
}
.error-text{
color: $red1;
font-size: 12px;
display: block;
padding-bottom: 0;
}
input[type="submit"]{
padding: 9px 35px;
}
}
.code-applied{
display: inline-block;
.green{
color: $green1;
font-weight: 600;
margin-right: 20px;
}
input[type="submit"]{
padding: 9px 35px;
background: white;
border: 2px solid $dark-gray2;
color: $dark-gray2;
box-shadow: none;
text-shadow: none;
&:hover{
background: white;
color: $dark-gray1;
border: 2px solid $dark-gray2;
}
}
}
input[type="submit"]{
width: auto;
padding: 7px 20px;
height: auto;
float: none;
font-size: 16px;
letter-spacing: 0;
font-weight: 600;
&:hover{
background: #1F8FC2;
border: 1px solid transparent;
box-shadow: none;
}
}
}
.col-two{
overflow: hidden;
padding-bottom: 20px;
border-bottom: 2px solid #f2f2f2;
.row-inside {
float: left;
width: 50%;
padding: 10px 0;
b{
font-size: 14px;
width: 190px;
display: inline-block;
margin-right: 20px;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
vertical-align: top;
}
label{
width: 300px;
margin: 0px;
display: inline-block;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-style: normal;
font-size: 14px;
word-wrap: break-word;
}
}
.col-1{
width: 35%;
float: left;
span.radio-group{
display: inline-block;
border: 2px solid #979797;
border-radius: 3px;
margin: 10px 0;
margin-left: 5px;
&:first-child{
margin-left: 15px;
}
&.blue{
border-color: $blue2;
label{
color: $blue2;
}
}
label{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-size: 16px;
font-style: normal;
color: $dark-gray2;
font-weight: 400;
padding: 8px 15px 8px 6px;
display: inline-block;
margin-bottom: 0;
}
}
input[type="radio"]{
margin-left: 10px;
}
}
.col-2{
width: 65%;
float: right;
input[type="submit"]{
width: auto;
padding: 18px 60px 22px 30px;
height: auto;
font-size: 24px;
letter-spacing: 0;
font-weight: 600;
margin-left: 15px;
&#register{
padding: 18px 30px;
}
}
p{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
padding: 13px 0;
text-align: right;
}
form{
position: relative;
}
i.icon-caret-right{
position: absolute;
right: 30px;
top: 25px;
color: white;
font-size: 24px;
}
label.pull-right{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-style: normal;
text-align: right;
padding: 10px 25px 10px;
display: inline-block;
float: right;
line-height: 20px;
color: $dark-gray1;
}
}
}
.disclaimer{
color: $light-gray2;
padding: 10px 0px;
text-align: right;
font-weight: 300;
}
h3{
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-size: 16px;
font-weight: 400;
padding: 30px 20px;
color: $dark-gray1;
}
.billing-data{
display: table;
width: 100%;
h3{
padding: 12px 0px;
color: $dark-gray1;
font-size: 17px;
margin-bottom: 5px;
}
.row{
display: table-row;
}
.col-half{
width: 45%;
float: left;
background: $light-gray1;
padding: 20px;
border-radius: 4px;
margin-bottom: 15px;
min-height: 240px;
&:nth-child(even){
margin-left: 30px;
}
.data-group{
margin-bottom: 15px;
label{
display: block;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
color: $dark-gray2;
}
input{width: 100%;margin-bottom: 5px;}
&:nth-child(4n){
margin-right: 0px;
}
}
}
}
.error-text{
color: $red1;
font-size: 12px;
display: block;
padding-bottom: 0;
}
.gray-bg{
background: $light-gray1;
border-radius: 3px;
padding: 20px 20px 20px 30px;
margin: 20px 0;
overflow: hidden;
.message-left{
float: left;
line-height: 24px;
color: $dark-gray1;
b{
text-transform: capitalize;
}
a.blue{
margin:0 0 0 20px;
i{
margin-left: 10px;
}
}
}
}
.bordered-bar{
border-bottom: 2px solid $light-gray1;
border-top: 2px solid $light-gray1;
margin-bottom: 20px;
padding: 20px;
h2{
color: $dark-gray1;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-weight: bold;
margin-bottom: 0;
font-size: 17px;
span{
padding-left: 60px;
text-transform: capitalize;
.blue-link{
color: $blue2;
font-size: 14px;
&:hover{
text-decoration: none;
}
}
}
}
}
.pattern{
margin-top: 10px;
margin-bottom: 20px;
padding:20px;
color: $dark-gray1;
}
hr.border{
border-top: 2px solid $light-gray1;
}
.no-border{border: none !important; }
table.course-receipt{
width: 94%;
margin: auto;
margin-bottom: 27px;
thead{
th{
color: $light-gray2;
font-weight: normal;
text-align: center;
text-transform: uppercase;
padding: 8px 0;
border-bottom: 1px solid $dark-gray2;
&:first-child{
text-align: left;
}
&:last-child{
text-align: right;
}
}
}
tr{
border-bottom: 1px solid $light-gray1;
&:last-child{
border-bottom: none;
}
td{
padding: 15px 0;
text-align: center;
color: $dark-gray1;
width: 33.33333%;
&:first-child{
text-align: left;
font-size: 18px;
text-transform: capitalize;
}
&:last-child{
text-align: right;
}
}
}
}
}
.empty-cart{
padding: 20px 0px;
background: $light-gray1;
text-align: center;
border-radius: 3px;
margin: 20px 0px;
h2{
font-size: 24PX;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;
font-weight: 600;
letter-spacing: 0;
color: #9b9b9b;
text-align: center;
margin-top: 20px;
text-transform: initial;
}
a.blue{
display: inline-block;
background: $blue2;
color: white;
padding: 20px 40px;
border-radius: 3px;
font-size: 24px;
font-weight: 400;
margin: 10px 0px 20px;
&:hover{
text-decoration: none;
}
}
}
// Print
@media print{
a[href]:after {
content: none !important;
}
ul.steps, a.blue.pull-right, .bordered-bar span.pull-right, .left.nav-global.authenticated {
display: none;
}
.shopping-cart{
font-size: 14px;
padding-right: 40px;
.gray-bg{
margin: 0;
padding: 10px 0 20px 0;
background: none;
.message-left{
width: 100%;
}
}
.bordered-bar{
h2{
font-size: 14px;
}
span{
float: right;
}
}
.user-data{
.data-input{
h1{
font-size: 18px;
}
}
}
}
}
<%! from django.utils.translation import ugettext as _ %>
<p>
${_("Hi {name},").format(name=recipient_name)}
</p>
<p>
${_("Thank you for your purchase of ")} <b> ${course_names} </b>
</p>
%if recipient_type == 'user':
<p>${_("Your payment was successful.")}</p>
% if marketing_link('FAQ'):
<p>${_("If you have billing questions, please read the FAQ ({faq_url}) or contact {billing_email}.").format(billing_email=payment_support_email, faq_url=marketing_link('FAQ'))}</p>
% else:
<p>${_("If you have billing questions, please contact {billing_email}.").format(billing_email=payment_support_email)}</p>
% endif
%elif recipient_type == 'company_contact':
<p>${_("{order_placed_by} placed an order and mentioned your name as the Organization contact.").format(order_placed_by=order_placed_by)}</p>
%elif recipient_type == 'email_recipient':
<p>${_("{order_placed_by} placed an order and mentioned your name as the additional receipt recipient.").format(order_placed_by=order_placed_by)}</p>
%endif
<p>${_("The items in your order are:")}</p>
<p>${_("Quantity - Description - Price")}<br>
%for order_item in order_items:
${order_item.qty} - ${order_item.line_desc} - ${"$" if order_item.currency == 'usd' else ""}${order_item.line_cost}</p>
%endfor
<p>${_("Total billed to credit/debit card: {currency_symbol}{total_cost}").format(total_cost=order.total_cost, currency_symbol=("$" if order.currency == 'usd' else ""))}</p>
<p>
% if order.company_name:
${_('Company Name:')} ${order.company_name}<br>
%endif
% if order.customer_reference_number:
${_('Purchase Order Number:')} ${order.customer_reference_number}<br>
%endif
% if order.company_contact_name:
${_('Company Contact Name:')} ${order.company_contact_name}<br>
%endif
% if order.company_contact_email:
${_('Company Contact Email:')} ${order.company_contact_email}<br>
%endif
% if order.recipient_name:
${_('Recipient Name:')} ${order.recipient_name}<br>
%endif
% if order.recipient_email:
${_('Recipient Email:')} ${order.recipient_email}<br>
%endif
% if has_billing_info:
${order.bill_to_cardtype} ${_("#:")} ${order.bill_to_ccnum}<br>
${order.bill_to_first} ${order.bill_to_last}<br>
${order.bill_to_street1}<br>
${order.bill_to_street2}<br>
${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode}<br>
${order.bill_to_country.upper()}
% endif
</p>
<p>${_("Order Number: {order_number}").format(order_number=order.id)}</p>
<p><b>${_("A CSV file of your registration URLs is attached. Please distribute registration URLs to each student planning to enroll using the email template below.")}</b></p>
<p>${_("Warm regards,")}<br>
% if payment_email_signature:
${payment_email_signature}
% else:
${_("The {platform_name} Team").format(platform_name=platform_name)}
%endif
</p>
———————————————————————————————————————————
<p>Dear [[Name]]</p>
<p>To enroll in ${course_names} we have provided a registration URL for you. Please follow the instructions below to claim your access.</p>
<p>Your redeem url is: [[Enter Redeem URL here from the attached CSV]]</p>
<p>${_("(1) Register for an account at <a href='https://{site_name}' >https://{site_name}</a>.").format(site_name=site_name)}<br>
${_("(2) Once registered, copy the redeem URL and paste it in your web browser.")}<br>
${_("(3) On the enrollment confirmation page, Click the 'Activate Enrollment Code' button. This will show the enrollment confirmation.")}<br>
${_("(4) You should be able to click on 'view course' button or see your course on your student dashboard at <a href='https://{dashboard_url}'>https://{dashboard_url}</a>").format(dashboard_url=dashboard_url)}<br>
${_("(5) Course materials will not be available until the course start date.")}</p>
<p>Sincerely,</p>
<p>[[Your Signature]]</p>
......@@ -4,11 +4,17 @@ ${_("Hi {name}").format(name=order.user.profile.name)}
${_("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)}
......
<%! 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) {
$.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);
} else {
$("#add_coupon_button").removeAttr('disabled');
},
error: function(jqXHR, textStatus, errorThrown) {
var data = $.parseJSON(jqXHR.responseText);
$('#add_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#add_coupon_form #coupon_form_error').text(xhr.responseText);
$('#add_coupon_form #coupon_form_error').text(data.message);
$("#add_coupon_button").removeAttr('disabled');
}
});
$('#edit_coupon_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true);
} else {
$("#update_coupon_button").removeAttr('disabled');
$('#edit_coupon_form #coupon_form_error').attr('style', 'display: block !important');
$('#edit_coupon_form #coupon_form_error').text(xhr.responseText);
}
});
// removing close link's default behavior
$('.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>
......
<%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>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%block name="pagetitle">${_("Your Shopping Cart")}</%block>
<section class="container cart-list">
<h2>${_("Your selected items:")}</h2>
<h3 class="cart-errors" id="cart-error">Error goes here.</h3>
% if shoppingcart_items:
<table class="cart-table">
<thead>
<tr class="cart-headings">
<th class="dsc">${_("Description")}</th>
<th class="u-pr">${_("Price")}</th>
<th class="cur">${_("Currency")}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
% for item in shoppingcart_items:
<tr class="cart-items">
<td>${item.line_desc}</td>
<td>
${"{0:0.2f}".format(item.unit_cost)}
% if item.list_price != None:
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
% endif
</td>
<td>${item.currency.upper()}</td>
<td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td>
</tr>
% endfor
<tr class="always-gray">
<td colspan="4" valign="middle" class="cart-total" align="right">
<b>${_("Total Amount")}: <span> ${"{0:0.2f}".format(amount)} </span> </b>
</td>
</tr>
</tbody>
<tfoot>
<tr class="always-white">
<td colspan="2">
<input type="text" placeholder="Enter code here" name="cart_code" id="code">
<input type="button" value="Apply Code" id="cart-code">
</td>
<td colspan="4" align="right">
% if amount == 0:
<input type="button" value = "Register" id="register" >
% else:
${form_html}
%endif
</td>
</tr>
</tfoot>
</table>
<!-- <input id="back_input" type="submit" value="Return" /> -->
% else:
<p>${_("You have selected no items for purchase.")}</p>
% endif
</section>
<script>
$(function() {
$('a.remove_line_item').click(function(event) {
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.remove_item')}";
$.post(post_url, {id:$(this).data('item-id')})
.always(function(data){
location.reload(true);
});
});
$('#cart-code').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.use_code')}";
$.post(post_url,{
"code" : $('#code').val(),
beforeSend: function(xhr, options){
if($('#code').val() == "") {
showErrorMsgs('Must enter a valid code')
xhr.abort();
}
}
}
)
.success(function(data) {
location.reload(true);
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText)
}
})
});
$('#register').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.register_courses')}";
$.post(post_url)
.success(function(data) {
window.location.href = "${reverse('dashboard')}";
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText)
}
})
});
$('#back_input').click(function(){
history.back();
});
function showErrorMsgs(msg){
$(".cart-errors").css('display', 'block');
$("#cart-error").html(msg);
}
});
</script>
\ No newline at end of file
<%inherit file="shopping_cart_flow.html" />
<%! from django.utils.translation import ugettext as _ %>
<%! from django.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="bodyclass">purchase-receipt</%block>
<%block name="pagetitle">${_("Register for [Course Name] | Receipt (Order")} ${order.id})</%block>
<%block name="billing_details_highlight">
% if order_type == 'business':
<li>${_('Billing Details')}</li>
%endif
</%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>
% if (len(shoppingcart_items) == 1 and order_type == 'personal') or receipt_has_donation_item:
% for inst in instructions:
<p>${inst}</p>
% endfor
% endif
</section>
<section class="wrapper confirm-enrollment shopping-cart print">
<div class="gray-bg">
<div class="message-left">
<% courses_url = reverse('courses') %>
% if order_type == 'personal':
## in case of multiple courses in single self purchase scenario,
## we will show the button View Dashboard
% if len(shoppingcart_items) > 1 :
<% dashboard_url = reverse('dashboard') %>
<a href="${dashboard_url}" class="blue pull-right">${_("View Dashboard")} <i class="icon-caret-right"></i></a>
% elif shoppingcart_items and shoppingcart_items[0][1]:
<% course = shoppingcart_items[0][1] %>
<% course_info_url = reverse('info', kwargs={'course_id': course.id.to_deprecated_string()}) %>
<a href="${course_info_url}" class="blue pull-right">${_("View Course")} <i class="icon-caret-right"></i></a>
%endif
${_("You have successfully been enrolled for <b>{appended_course_names}</b>. The following receipt has been emailed to"
" <strong>{appended_recipient_emails}</strong>").format(appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)}
% elif order_type == 'business':
% if total_registration_codes > 1 :
<% code_plural_form = 'codes' %>
% else:
<% code_plural_form = 'code' %>
% endif
${_("You have successfully purchased <b>{total_registration_codes} course registration codes</b> "
"for <b>{appended_course_names}. </b>"
"The following receipt has been emailed to <strong>{appended_recipient_emails}</strong>"
).format(total_registration_codes=total_registration_codes, appended_course_names=appended_course_names, appended_recipient_emails=appended_recipient_emails)}
% endif
<section class="wrapper cart-list">
<div 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">
</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 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>
<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>
% for item in order_items:
% endfor
</tbody>
</table>
%endif
<div class="bordered-bar">
<h2>${_('Invoice')} #${order.id}<span>${_('Date of purchase')}: ${order_purchase_date} </span><span
class="pull-right"><a href="" onclick="window.print();" class="blue-link"><i class="icon-print"></i> ${_('Print Receipt')}</a></span>
</h2>
</div>
% if order.total_cost > 0:
<div class="pattern">
<h2> ${_("Billed To Details")}: </h2>
<div class="col-two no-border">
% if order_type == 'business':
<div class="row">
<div class="row-inside">
<p>
<b>${_('Company Name')}:</b>
<label>
% if order.company_name:
${_("{company_name}").format(company_name=order.company_name)}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Purchase Order Number')}:</b>
<label>
% if order.customer_reference_number:
${_("{customer_reference_number}").format(customer_reference_number=order.customer_reference_number)}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Company Contact Name')}:</b>
<label>
% if order.company_contact_name:
${_("{company_contact_name}").format(company_contact_name=order.company_contact_name)}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Company Contact Email')}:</b>
<label>
% if order.company_contact_email:
${ order.company_contact_email }
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Recipient Name')}:</b>
<label>
% if order.recipient_name:
${_("{recipient_name}").format(recipient_name=order.recipient_name)}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Recipient Email')}:</b>
<label>
% if order.recipient_email:
${order.recipient_email}
% else:
N/A
% endif
</label>
</p>
</div>
</div>
%endif
<div class="row">
<div class="row-inside">
<p>
<b>${_('Card Type')}:</b>
<label>
% if order.bill_to_cardtype:
${order.bill_to_cardtype}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Credit Card Number')}:</b>
<label>
% if order.bill_to_ccnum:
${order.bill_to_ccnum}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Name')}:</b>
<label>
% if order.bill_to_first or order.bill_to_last:
${order.bill_to_first} ${order.bill_to_last}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Address 1')}:</b>
<label>
% if order.bill_to_street1:
${order.bill_to_street1}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Address 2')}:</b>
<label>
% if order.bill_to_street2:
${order.bill_to_street2}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('City')}:</b>
<label>
% if order.bill_to_city:
${order.bill_to_city}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('State')}:</b>
<label>
% if order.bill_to_state:
${order.bill_to_state}
% else:
N/A
% endif
</label>
</p>
</div>
<div class="row-inside">
<p>
<b>${_('Country')}:</b>
<label>
% if order.bill_to_country:
${order.bill_to_country.upper()}
% else:
N/A
% endif
</label>
</p>
</div>
</div>
</div>
</div>
% endif
<hr class="border"/>
% for item, course in shoppingcart_items:
% if loop.index > 0 :
<hr>
%endif
<div class="user-data">
<div class="clearfix">
<div class="image">
<img style="width: 100%; height: 100%;" src="${course_image_url(course)}"
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Image"/>
</div>
<div class="data-input">
<h3>${_("Registration for")}:
<span class="pull-right">
% if course.start_date_text or course.end_date_text:
${_("Course Dates")}:
%endif
</span>
</h3>
<tr class="order-item">
<h1>${_(" {course_name} ").format(course_name=course.display_name)}
<span class="pull-right">
% if course.start_date_text:
${course.start_date_text}
%endif
-
% if course.end_date_text:
${course.end_date_text}
%endif
</span>
</h1>
<hr/>
<div class="three-col">
% if item.status == "purchased":
<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)}
<div class="col-1">
% if item.list_price != None:
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
<div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span>
</div>
<div class="price green">${_('Discount Applied:')} <span> $${"{0:0.2f}".format(item.unit_cost)} </span></div>
% else:
<div class="price">${_('Price per student:')} <span> $${"{0:0.2f}".format(item.unit_cost)}</span></div>
% endif
</td>
<td>${"{0:0.2f}".format(item.line_cost)}</td>
<td>${item.currency.upper()}</td></tr>
</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":
<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>
<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
<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>
<div class="discount">
<div class="code-text">
% if any_refunds:
<p>
<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.")}
</p>
% 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>
</span>
% endif
</article>
<span class="pull-right">${_("Total")}: <b>$${"{0:0.2f}".format(order.total_cost)} USD</b></span>
</div>
</div>
</section>
</div>
......
<%inherit file="shopping_cart_flow.html" />
<%block name="review_highlight">class="active"</%block>
<%!
from courseware.courses import course_image_url, get_course_about_section
from django.core.urlresolvers import reverse
from edxmako.shortcuts import marketing_link
from django.utils.translation import ugettext as _
%>
<%block name="custom_content">
<div class="container">
% if shoppingcart_items:
<%block name="billing_details_highlight">
% if order.order_type == 'business':
<li>${_('Billing Details')}</li>
% endif
</%block>
<% discount_applied = False %>
<section class="wrapper confirm-enrollment shopping-cart">
% for item, course in shoppingcart_items:
% if loop.index > 0 :
<hr>
%endif
<div class="user-data">
<div class="clearfix">
<div class="image">
<img style="width: 100%; height: 100%;" src="${course_image_url(course)}"
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Cover Image" />
</div>
<div class="data-input">
<h3>${_('Registration for:')} <span class="pull-right">${_('Course Dates:')}</span></h3>
<h1>${ course.display_name }<span class="pull-right">${course.start_date_text} - ${course.end_date_text}</span></h1>
<hr />
<div class="three-col">
<div class="col-1">
% if item.list_price != None:
<% discount_applied = True %>
<div class="price">${_('Price per student:')} <span class="line-through"> $${"{0:0.2f}".format(item.list_price)}</span></div>
<div class="price green">${_('Discount Applied:')} <span> $${"{0:0.2f}".format(item.unit_cost)} </span></div>
% else:
<div class="price">${_('Price per student:')} <span> $${"{0:0.2f}".format(item.unit_cost)}</span></div>
% endif
</div>
<div class="col-2">
<div class="numbers-row">
<label for="students">${_('Students:')}</label>
<div class="counter">
<input maxlength="3" max="999" type="text" name="students" value="${item.qty}" id="${item.id}" >
</div>
<div class="inc button"><i class="icon-caret-up"><span>+</span></i></div><div class="dec button"><i class="icon-caret-down"></i></div>
<a name="updateBtn" class="updateBtn hidden" id="updateBtn-${item.id}" href="#">update</a>
<span class="error-text hidden" id="students-${item.id}"></span>
</div>
</div>
<div class="col-3">
<a href="#" class="btn-remove" data-item-id="${item.id}"><i class="icon-remove-sign"></i></a>
</div>
</div>
</div>
</div>
</div>
% endfor
<div class="discount">
<div class="code-text">
% if not discount_applied:
<div class="code-input">
<input type="text" placeholder="discount or activation code" id="input_code">
<input type="submit" value="Apply" class="blue" id="submit-code">
<span class="error-text hidden" id="code" ></span>
</div>
% else:
<div class="code-applied">
<span class="green"><i class="icon-ok"></i>${_('code has been applied')}</span>
<input type="submit" value="Reset" class="blue-border" id="submit-reset-redemption">
</div>
%endif
<span class="pull-right">${_('Total:')} <b id="total-amount">$${"{0:0.2f}".format(amount)} USD</b></span>
</div>
</div>
<div class="col-two">
<div class="col-2 relative">
% if amount == 0:
<input type="submit" value = "Register" id="register" >
% elif item.order.order_type == 'business':
<input type="submit" value = "Billing Details" id="billing-details"><i class="icon-caret-right"></i>
<p>
${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')}
</p>
% else:
${form_html}
<p>
${_('After this purchase is complete,')}<br/><b>${order.user.username}</b>
${_('will be enrolled in this course.')}
</p>
%endif
</div>
</div>
</section>
% else:
<div class="empty-cart" >
<h2>${_('Your Shopping cart is currently empty.')}</h2>
<a href="${marketing_link('COURSES')}" class="blue">${_('View Courses')}</a>
</div>
% endif
</div>
</%block>
<script>
$(function() {
$('a.btn-remove').click(function(event) {
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.remove_item')}";
$.post(post_url, {id:$(this).data('item-id')})
.always(function(data){
location.reload(true);
});
});
$('#submit-code').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.use_code')}";
if($('#input_code').val() == "") {
showErrorMsgs('Must enter a valid code','code');
return;
}
$.post(post_url,{
"code" : $('#input_code').val()
}
)
.success(function(data) {
location.reload(true);
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText, 'code')
}
})
});
$('#submit-reset-redemption').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.reset_code_redemption')}";
$.post(post_url)
.success(function(data) {
location.reload(true);
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText,'code')
}
})
});
$('#register').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.register_courses')}";
$.post(post_url)
.success(function(data) {
window.location.href = "${reverse('dashboard')}";
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText)
}
})
});
$('#billing-details').click(function(event){
event.preventDefault();
location.href = "${reverse('shoppingcart.views.billing_details')}";
});
$(".button").on("click", function() {
var studentField = $(this).parent().find('input');
var ItemId = studentField.attr('id');
var $button = $(this);
var oldValue = $button.parent().find("input").val();
var newVal = 1; // initialize with 1.
hideErrorMsg('students-'+ItemId);
if ($.isNumeric(oldValue)){
if ($button.text() == "+") {
if(oldValue > 0){
newVal = parseFloat(oldValue) + 1;
if(newVal > 1000){
newVal = 1000;
}
}
} else {
// Don't allow decrementing below one
if (oldValue > 1) {
newVal = parseFloat(oldValue) - 1;
}
}
}
$button.parent().find("input").val(newVal);
$('#updateBtn-'+ItemId).removeClass('hidden');
});
$('a[name="updateBtn"]').click(function(event) {
var studentField = $(this).parent().find('input');
var number_of_students = studentField.val();
var ItemId = studentField.attr('id');
if($.isNumeric(number_of_students) && number_of_students > 0 ){
hideErrorMsg('students-'+ItemId);
update_user_cart(ItemId, number_of_students);
}else{
showErrorMsgs('quantity must be greater then 0.', 'students-'+ItemId);
}
});
function showErrorMsgs(msg, msg_area){
$( "span.error-text#"+ msg_area +"" ).removeClass("hidden");
$( "span.error-text#"+ msg_area +"" ).html(msg).show();
if(msg_area=='code'){
$("#input_code").addClass('error');
}
}
function hideErrorMsg(msg_area){
$( "span.error-text#"+ msg_area +"" ).addClass("hidden");
}
function update_user_cart(ItemId, number_of_students){
var post_url = "${reverse('shoppingcart.views.update_user_cart')}";
$.post(post_url, {
ItemId:ItemId,
qty:number_of_students
}
)
.success(function(data) {
location.reload(true);
})
.error(function(data,status) {
location.reload(true);
})
}
$('input[name="students"]').on("click", function() {
$('#updateBtn-'+this.id).removeClass('hidden');
});
// allowing user to enter numeric qty only.
$("input[name=students]").keydown(function(event) {
var eventDelete = 46;
var eventBackSpace = 8;
var eventLeftKey = 37;
var eventRightKey = 39;
var allowedEventCodes = [eventDelete, eventBackSpace, eventLeftKey, eventRightKey ];
// Allow only backspace and delete
if (allowedEventCodes.indexOf(event.keyCode) > -1) {
// let it happen, don't do anything
}
else {
/*
Ensure that it is a number.
KeyCode range 48 - 57 represents [0-9]
KeyCode range 96 - 105 represents [numpad 0 - numpad 9]
*/
if ((event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >= 96 && event.keyCode <= 105) ) {
$('#updateBtn-'+this.id).removeClass('hidden');
}else{
event.preventDefault();
}
}
});
});
</script>
<%!
from django.utils.translation import ugettext as _
%>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("Shopping cart")}</%block>
<%block name="bodyextra">
<div class="container">
<section class="wrapper confirm-enrollment shopping-cart">
<h1> ${_("{site_name} - Shopping Cart").format(site_name=site_name)}</h1>
% if shoppingcart_items:
<ul class="steps">
<li <%block name="review_highlight"/>>${_('Review')}</li>
<%block name="billing_details_highlight"/>
<li <%block name="payment_highlight"/>>${_('Payment')}</li>
<li <%block name="confirmation_highlight"/>>${_('Confirmation')}</li>
</ul>
%endif
</section>
</div>
<%block name="custom_content"/>
</%block>
......@@ -103,7 +103,7 @@
</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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment