Commit 1abf064d by chrisndodge

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

Cdodge/shopping cart rewrite
parents be19c1a9 4f7c4949
......@@ -446,6 +446,10 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
"""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